2026-03-26 12:07:55 +00:00

478 lines
14 KiB
PHP

<?php
/**
* Simply Schedule Appointments Settings.
*
* @since 0.0.3
* @package Simply_Schedule_Appointments
*/
/**
* Simply Schedule Appointments Settings.
*
* @since 0.0.3
*/
class SSA_Settings {
/**
* Parent plugin class.
*
* @since 0.0.3
*
* @var Simply_Schedule_Appointments
*/
protected $plugin = null;
protected $option_name = 'ssa_settings_json';
protected $defaults = null;
protected $schema;
protected $settings;
protected $computed_schema = array();
/**
* Constructor
*
* @since 0.0.3
* @param object $plugin Main plugin object.
* @return void
*/
public function __construct( $plugin ) {
$this->plugin = $plugin;
$this->hooks();
}
/**
* Initiate our hooks
*
* @since 0.0.3
* @return void
*/
public function hooks() {
}
public function get_schema() {
if ( !empty( $this->schema ) ) {
return $this->schema;
}
$schema = apply_filters( 'ssa_settings_schema', array() );
$this->schema = $schema;
return $this->schema;
}
public function get_computed_schema() {
if ( !empty( $this->computed_schema ) ) {
return $this->computed_schema;
}
$computed_schema = apply_filters( 'ssa_settings_computed_schema', array() );
$this->computed_schema = $computed_schema;
return $this->computed_schema;
}
public function get() {
if ( !empty( $this->settings ) ) {
// $this->add_computed_values(); // we shouldn't need to re-compute values on every run through this function
return $this->settings;
}
$settings_json = get_option( $this->option_name, json_encode( array() ) );
$settings = json_decode( $settings_json, true );
$schema = $this->get_schema(); // load the schema so that we can load default values as necessary
foreach ($schema as $section_key => $section_schema) {
if ( !empty( $settings[$section_key]['schema_version'] )
&& $settings[$section_key]['schema_version'] >= $schema[$section_key]['version'] ) {
// In this case we already have saved data with this schema, so we do not need to load any default values
continue;
}
if ( empty( $settings[$section_key]['schema_version'] ) ) {
// this is the first time that we have encountered this schema
$settings[$section_key] = array();
}
// let's start merging in default values
$default_values_from_schema = array_combine(
wp_list_pluck( $schema[$section_key]['fields'], 'name' ),
wp_list_pluck( $schema[$section_key]['fields'], 'default_value' )
);
if ( empty( $settings[$section_key] ) ) {
$settings[$section_key] = $default_values_from_schema;
} else {
$settings[$section_key] = array_merge( $default_values_from_schema, $settings[$section_key] );
}
$settings[$section_key]['schema_version'] = $schema[$section_key]['version'];
}
if ( empty( $settings['global']['timezone_string'] ) ) {
$settings['global']['timezone_string'] = 'UTC';
}
$this->settings = $settings;
$this->add_computed_values();
$this->add_enabled_activated_values();
$this->decrypt();
return $this->settings;
}
public function remove_unauthorized_settings_for_current_user( $settings, $replace_tokens = true, $unset_admin_only_fields = false ) {
foreach ($settings as $module_slug => $module_settings) {
if ( empty( $module_settings ) || ! is_array( $module_settings ) ) {
continue;
}
if ( 'global' === $module_slug ) {
$module_settings_slug = 'settings_global';
} else {
$module_settings_slug = $module_slug.'_settings';
}
if('license' === $module_slug && defined('SSA_LICENSE_KEY') && !empty(SSA_LICENSE_KEY)){
$settings['license']['license'] = "redacted-writeonly-secret";
$settings['license']['license_filtered'] = "redacted-writeonly-secret";
$settings['license']['license_renewal_link'] = "redacted-writeonly-secret";
}
foreach ($module_settings as $field_slug => $module_setting_value) {
$module_schema = $this->plugin->$module_settings_slug->get_schema();
$computed_schema = $this->plugin->$module_settings_slug->get_computed_schema();
if ( empty( $module_schema['fields'] ) ) {
continue;
}
// this is only doable with fields that the frontend will never need to write, read, or update
// it will not work for something like Stripe keys
if( true == $replace_tokens && isset( $module_schema['fields'][$field_slug]['writeonly_secret'] ) && true == $module_schema['fields'][$field_slug]['writeonly_secret'] ){
if( !empty( $settings[$module_slug][$field_slug] ) ){
$settings[$module_slug][$field_slug]="redacted-writeonly-secret";
continue;
}
}
if ( ! empty( $module_schema['fields'][$field_slug]['required_capability'] ) ) {
if ( ! current_user_can( $module_schema['fields'][$field_slug]['required_capability'] ) || $unset_admin_only_fields ) {
unset( $settings[$module_slug][$field_slug] );
}
}
if ( ! empty( $computed_schema['fields'][$field_slug]['required_capability'] ) ) {
if ( ! current_user_can( $computed_schema['fields'][$field_slug]['required_capability'] ) || $unset_admin_only_fields ) {
unset( $settings[$module_slug][$field_slug] );
}
}
}
}
return $settings;
}
public function add_computed_values() {
$computed_schema = $this->get_computed_schema(); // load the computed_schema so that we can calculate necessary values
foreach ($computed_schema as $section_key => $section_computed_schema) {
if ( empty( $section_computed_schema['fields'] ) ) {
continue;
}
foreach ($section_computed_schema['fields'] as $computed_field ) {
if ( !empty( $computed_field['get_input'] ) ) {
$input = $computed_field['get_input'];
} elseif ( !empty( $computed_field['get_input_path'] ) && isset( $this->settings[$section_key][$computed_field['get_input_path']] ) ) {
$input = $this->settings[$section_key][$computed_field['get_input_path']];
} else {
$input = null;
}
$computed_value = call_user_func( $computed_field['get_function'], $input );
$this->settings[$section_key][$computed_field['name']] = $computed_value;
}
}
}
public function add_enabled_activated_values() {
$schema = $this->get_schema(); // load the schema so that we can calculate necessary values
foreach ($schema as $section_key => $section_schema) {
if ( empty( $section_schema['fields'] ) ) {
continue;
}
$this->settings[$section_key]['enabled'] = $this->plugin->settings_installed->is_enabled( $section_key );
}
}
public function decrypt() {
if ( ! function_exists( 'openssl_decrypt' ) ) {
return;
}
$schema = $this->get_schema(); // load the schema so that we can calculate necessary values
foreach ($schema as $section_key => $section_schema) {
if ( empty( $section_schema['fields'] ) ) {
continue;
}
foreach ($section_schema['fields'] as $field ) {
if ( empty( $field['encrypt'] ) ) {
continue;
}
if ( empty( $this->settings[$section_key][$field['name']] ) ) {
continue;
}
if ( false === strpos( $this->settings[$section_key][$field['name']], SSA_Encryption::SALT ) ) {
continue; // confirm that this indeed encrypted data
}
$this->settings[$section_key][$field['name']] = str_replace( SSA_Encryption::SALT, '', $this->settings[$section_key][$field['name']] );
// $decrypted_value = call_user_func( $field['get_function'], $input );
$this->settings[$section_key][$field['name']] = SSA_Encryption::decrypt( $this->settings[$section_key][$field['name']] );
}
}
}
public function get_encrypted_settings( $new_settings ) {
if ( ! function_exists( 'openssl_encrypt' ) ) {
return $new_settings;
}
$schema = $this->get_schema(); // load the schema so that we can calculate necessary values
foreach ($schema as $section_key => $section_schema) {
if ( empty( $section_schema['fields'] ) ) {
continue;
}
foreach ($section_schema['fields'] as $field ) {
if ( empty( $field['encrypt'] ) ) {
continue;
}
if ( empty( $new_settings[$section_key][$field['name']] ) ) {
continue;
}
// $decrypted_value = call_user_func( $field['get_function'], $input );
$new_settings[$section_key][$field['name']] = SSA_Encryption::encrypt( $new_settings[$section_key][$field['name']] ) . SSA_Encryption::SALT;
}
}
return $new_settings;
}
public function update( $new_settings ) {
$existing_settings = $this->get();
$merged_settings = shortcode_atts( $existing_settings, $new_settings );
$merged_settings['last_updated'] = gmdate( 'Y-m-d H:i:s' );
return $this->set( $merged_settings );
}
public function update_section( $section_key, $new_settings ) {
$new_settings = $this->cleanup_writeonly_secret_values( $section_key, $new_settings );
$settings = $this->get();
$old_settings = $settings;
if ( empty( $settings[$section_key] ) ) {
return false;
}
$validation = $this->plugin->{$section_key.'_settings'}->validate( $new_settings, $old_settings[$section_key] );
if ( is_wp_error( $validation ) ) {
// we have an error with the inputs
return $validation;
}
$settings[$section_key] = shortcode_atts( $settings[$section_key], $new_settings );
$computed_schema = $this->get_computed_schema();
if ( !empty( $computed_schema[$section_key]['fields'] ) ) {
foreach ($computed_schema[$section_key]['fields'] as $computed_field ) {
if ( !isset( $settings[$section_key][$computed_field['name']])) {
continue;
}
if ( !empty( $settings[$section_key][$computed_field['name']] ) ) {
if ( !empty( $computed_field['set_result_path'] ) ) {
if ( isset( $settings[$section_key][$computed_field['set_result_path']] ) ) {
$result_value = call_user_func( $computed_field['set_function'], $settings[$section_key][$computed_field['name']] );
$settings[$section_key][$computed_field['set_result_path']] = $result_value;
}
}
}
unset( $settings[$section_key][$computed_field['name']] );
}
}
/* before_save */
$schema = $this->get_schema();
if ( !empty( $schema[$section_key]['fields'] ) ) {
foreach ($schema[$section_key]['fields'] as $field) {
if ( empty( $field['before_save_function'] ) ) {
continue;
}
if ( !isset( $settings[$section_key][$field['name']])) {
continue;
}
if ( !empty( $settings[$section_key][$field['name']] ) ) {
if ( !empty( $field['before_save_function'] ) ) {
$result_value = call_user_func( $field['before_save_function'], $settings[$section_key][$field['name']] );
$settings[$section_key][$field['name']] = $result_value;
}
}
}
}
$settings[$section_key]['last_updated'] = gmdate( 'Y-m-d H:i:s' );
$this->set( $settings );
$settings = $this->get();
do_action( 'ssa/settings/'.$section_key.'/updated', $settings[$section_key], $old_settings[$section_key] );
return $settings[$section_key];
}
private function set( $new_settings ) {
$this->settings = $new_settings;
$encrypted_settings = $this->get_encrypted_settings( $new_settings );
update_option( $this->option_name, json_encode( $encrypted_settings ) );
$this->add_computed_values();
$this->add_enabled_activated_values();
return $this->settings;
}
public function cleanup_writeonly_secret_values( $section_key, $settings ){
$schema = $this->get_schema()[$section_key]['fields'];
if( is_array( $settings ) ){
foreach ( $settings as $field => $value ) {
if ( isset( $schema[ $field ]['writeonly_secret'] ) && true == $schema[ $field ]['writeonly_secret'] && 'redacted-writeonly-secret' == $value){
unset( $settings[$field] );
}
}
}
return $settings;
}
}
abstract class SSA_Settings_Schema {
protected $schema = array();
protected $computed_schema = array();
protected $slug;
protected $parent_slug;
protected $defaults;
abstract function get_schema();
public function get_computed_schema() {
return array();
}
public function __construct() {
if ( empty( $this->slug ) ) {
die( 'no slug defined for: '.get_class( $this ).' (this slug will be used as the key to save in to the settings array)' ); // phpcs:ignore
}
$this->parent_hooks();
}
public function parent_hooks() {
add_filter( 'ssa_settings_schema', array( $this, 'filter_settings_schema' ) );
add_filter( 'ssa_settings_computed_schema', array( $this, 'filter_settings_computed_schema' ) );
}
public function filter_settings_schema( $schema ) {
$schema[$this->slug] = $this->get_schema();
return $schema;
}
public function filter_settings_computed_schema( $schema ) {
$schema[$this->slug] = $this->get_computed_schema();
return $schema;
}
public function get_field_defaults() {
if ( !empty( $this->defaults ) ) {
return $this->defaults;
}
$defaults = array();
$schema = $this->get_schema();
if ( empty( $schema['fields'] ) ) {
return $defaults;
}
$defaults = array_combine(
wp_list_pluck( $schema['fields'], 'name' ),
wp_list_pluck( $schema['fields'], 'default_value' )
);
$this->defaults = $defaults;
return $this->defaults;
}
public function get() {
if ( $this->slug !== 'installed' ) {
if ( ! $this->plugin->settings_installed->is_enabled( $this->slug ) ) {
return null;
}
if ( ! empty( $this->parent_slug ) ) {
if ( ! $this->plugin->settings_installed->is_enabled( $this->parent_slug ) ) {
return null;
}
}
}
return $this->plugin->settings->get()[$this->slug];
}
public function reset_to_defaults( $re_enable_feature = true ) {
$defaults = $this->get_field_defaults();
if ( !empty( $re_enable_feature ) ) {
$defaults['enabled'] = $re_enable_feature;
}
return $this->update( $defaults );
}
public function update( $new_settings ) {
// $old_settings = $this->get();
// $new_settings = apply_filters( 'update_'.$this->slug.'_settings', $new_settings, $old_settings );
return $this->plugin->settings->update_section( $this->slug, $new_settings );
}
public function validate( $new_settings, $old_settings ) {
if ( empty( $new_settings ) ) {
return;
}
$wp_error = new WP_Error();
$schema = $this->get_schema()['fields'];
foreach ( $new_settings as $field => $value ) {
if ( isset( $schema[ $field ]['validate_callback'] ) ) {
$validation_result = call_user_func( $schema[ $field ]['validate_callback'], $value );
if ( $validation_result !== true ) {
if ( empty( $wp_error->errors ) ) {
// populate the general error message
$wp_error->add( 422, $this->slug . ' ' . __( 'settings are invalid' ) );
}
// add the field specific error message - $validation_result is already translated
$wp_error->add( 422, array( $field, $validation_result ) );
}
}
}
if ( ! empty( $wp_error->errors ) ) {
return $wp_error;
}
}
}