get_allowed_settings_keys() as $key ) { $settings[ $key ] = $this->sanitize_setting( $key, $this->get_setting( $key ) ); } return $settings; } /** * Get the plugin's settings array. * * @param bool $force * * @return array */ public function get_settings( bool $force = false ): array { if ( empty( $this->settings ) || $force ) { $this->settings = array(); $saved_settings = get_site_option( static::SETTINGS_KEY ); $saved_settings = ! empty( $saved_settings ) && is_array( $saved_settings ) ? $saved_settings : array(); $this->settings = $this->filter_settings( $saved_settings ); // Now that we have merged database and defined settings, sanitize them before use. if ( ! empty( $this->settings ) ) { foreach ( $this->settings as $key => $val ) { $this->settings[ $key ] = $this->sanitize_setting( $key, $val ); } } // If defined settings keys have changed since last time settings were saved to database, // re-save to remove the new keys. if ( ! empty( $saved_settings ) && ! empty( $this->defined_settings ) && ! empty( array_intersect_key( $saved_settings, $this->defined_settings ) ) ) { $this->save_settings(); } } return $this->settings; } /** * Gets a single setting that has been defined in the plugin settings constant. * * @param string $key * @param mixed $default * * @return mixed */ public function get_defined_setting( string $key, $default = '' ) { $defined_settings = $this->get_defined_settings(); return isset( $defined_settings[ $key ] ) ? $defined_settings[ $key ] : $default; } /** * Get all settings that have been defined via constant for the plugin. * * @param bool $force * * @return array */ public function get_defined_settings( bool $force = false ): array { if ( empty( $this->defined_settings ) || $force ) { $this->defined_settings = array(); if ( ! static::settings_constant() ) { $unserialized = array(); } else { $unserialized = AS3CF_Utils::maybe_unserialize( constant( static::settings_constant() ) ); } $unserialized = is_array( $unserialized ) ? $unserialized : array(); if ( method_exists( $this, 'get_legacy_defined_settings' ) ) { $unserialized = $this->get_legacy_defined_settings( $unserialized ); } // Nothing in new style or legacy defines, we're done. if ( empty( $unserialized ) ) { return $this->defined_settings; } foreach ( $unserialized as $key => $value ) { if ( ! in_array( $key, $this->get_allowed_settings_keys( true ) ) ) { continue; } $value = $this->sanitize_setting( $key, $value ); $this->defined_settings[ $key ] = $value; } // Normalize the defined settings before saving, so we can detect when a real change happens. ksort( $this->defined_settings ); // If only legacy defines are in use, we can fake new style to allow for key based monitoring and db settings cleanup. if ( ! static::settings_constant() ) { static::$legacy_defines = true; define( static::preferred_settings_constant(), serialize( $this->defined_settings ) ); } $this->listen_for_settings_constant_changes(); update_site_option( 'as3cf_constant_' . static::settings_constant(), array_diff_key( $this->defined_settings, array_flip( $this->get_monitored_settings_blacklist() ) ) ); } return $this->defined_settings; } /** * Are only legacy defines in use? * * @return bool */ public static function using_legacy_defines(): bool { return static::$legacy_defines; } /** * Returns first (preferred) settings constant that can be defined, otherwise blank. * * @return string */ public static function preferred_settings_constant(): string { if ( ! empty( static::$settings_constants ) ) { return static::$settings_constants[0]; } else { return ''; } } /** * Get the constant used to define the settings. * * @return string|false Constant name if defined, otherwise false */ public static function settings_constant() { return AS3CF_Utils::get_first_defined_constant( static::$settings_constants ); } /** * Subscribe to a change of the site option used to store the constant-defined settings. */ protected function listen_for_settings_constant_changes() { if ( false !== static::settings_constant() && ! has_action( 'update_site_option_as3cf_constant_' . static::settings_constant(), array( $this, 'settings_constant_changed', ) ) ) { add_action( 'add_site_option_as3cf_constant_' . static::settings_constant(), array( $this, 'settings_constant_added', ), 10, 3 ); add_action( 'update_site_option_as3cf_constant_' . static::settings_constant(), array( $this, 'settings_constant_changed', ), 10, 4 ); } } /** * Translate a settings constant option addition into a change. * * @param string $option Name of the option. * @param mixed $value Value the option is being initialized with. * @param int $network_id ID of the network. */ public function settings_constant_added( string $option, $value, int $network_id ) { $db_settings = get_site_option( static::SETTINGS_KEY, array() ); $this->settings_constant_changed( $option, $value, $db_settings, $network_id ); } /** * Callback for announcing when settings-defined values change. * * @param string $option Name of the option. * @param mixed $new_settings Current value of the option. * @param mixed $old_settings Old value of the option. * @param int $network_id ID of the network. */ public function settings_constant_changed( string $option, $new_settings, $old_settings, int $network_id ) { if ( ! static::settings_constant() ) { return; } $old_settings = $old_settings ? $old_settings : array(); foreach ( $this->get_allowed_settings_keys( true ) as $setting ) { $old_value = isset( $old_settings[ $setting ] ) ? $old_settings[ $setting ] : null; $new_value = isset( $new_settings[ $setting ] ) ? $new_settings[ $setting ] : null; if ( $old_value !== $new_value ) { /** * Setting-specific hook for setting change. * * @param mixed $new_value * @param mixed $old_value * @param string $setting */ do_action( 'as3cf_constant_' . static::settings_constant() . '_changed_' . $setting, $new_value, $old_value, $setting ); /** * Generic hook for setting change. * * @param mixed $new_value * @param mixed $old_value * @param string $setting */ do_action( 'as3cf_constant_' . static::settings_constant() . '_changed', $new_value, $old_value, $setting ); } } } /** * Filter in defined settings with sensible defaults. * * @param array $settings * * @return array $settings */ public function filter_settings( array $settings ): array { $defined_settings = $this->get_defined_settings(); // Bail early if there are no defined settings. if ( empty( $defined_settings ) ) { return $settings; } foreach ( $defined_settings as $key => $value ) { $allowed_values = array(); if ( in_array( $key, $this->get_boolean_format_settings() ) ) { $allowed_values = array( true, false, '0', '1' ); } // Unexpected value, remove from defined_settings array. if ( ! empty( $allowed_values ) && ! in_array( $value, $allowed_values ) ) { $this->remove_defined_setting( $key ); continue; } // Value defined successfully. $settings[ $key ] = $value; } return $settings; } /** * Ensure that sensitive settings are obfuscated. * * @param array $settings * * @return array */ public function obfuscate_sensitive_settings( array $settings ): array { $sensitive_settings = $this->get_sensitive_settings(); if ( empty( $sensitive_settings ) ) { return $settings; } foreach ( $settings as $key => $value ) { if ( ! empty( $value ) && in_array( $key, $sensitive_settings ) ) { $settings[ $key ] = _x( '-- not shown --', 'placeholder for sensitive setting, e.g. secret access key', 'amazon-s3-and-cloudfront' ); } } return $settings; } /** * Sanitize a setting value, maybe. * * @param string $key * @param mixed $value * * @return array|bool|string */ public function sanitize_setting( string $key, $value ) { $skip_sanitize = $this->get_skip_sanitize_settings(); $boolean_settings = $this->get_boolean_format_settings(); if ( in_array( $key, $skip_sanitize ) ) { if ( is_array( $value ) ) { $result = array(); foreach ( $value as $k => $v ) { $result[ $k ] = wp_strip_all_tags( $v ); } $value = $result; } else { $value = wp_strip_all_tags( $value ); } } elseif ( in_array( $key, $boolean_settings ) ) { $value = (bool) $value; } elseif ( is_numeric( $value ) ) { $value = strval( $value ); } else { $value = sanitize_text_field( $value ); // Make sure path setting is absolute and not just "/". // But not on Windows as it can have various forms of path, e.g. C:\Sites and \\shared\sites. if ( '/' === DIRECTORY_SEPARATOR && in_array( $key, $this->get_path_format_settings() ) ) { $value = trim( AS3CF_Utils::unleadingslashit( $value ) ); $value = empty( $value ) ? '' : AS3CF_Utils::leadingslashit( $value ); } // Make sure prefix setting is relative with trailing slash for visibility. if ( in_array( $key, $this->get_prefix_format_settings() ) ) { $value = trim( untrailingslashit( $value ) ); $value = empty( $value ) ? '' : AS3CF_Utils::trailingslash_prefix( $value ); } } return $value; } /** * Set a setting. * * @param string $key * @param mixed $value */ public function set_setting( string $key, $value ) { $this->get_settings(); $this->settings[ $key ] = $value; } /** * Bulk set the settings array. * * @param array $settings */ public function set_settings( array $settings ) { $this->settings = $settings; } /** * Save the settings to the database. */ public function save_settings() { if ( is_array( $this->settings ) ) { ksort( $this->settings ); } $this->update_site_option( static::SETTINGS_KEY, array_diff_key( $this->settings, $this->defined_settings ) ); } /** * Update site option. * * @param string $option * @param mixed $value * @param bool $autoload * * @return bool */ public function update_site_option( string $option, $value, bool $autoload = true ): bool { if ( is_multisite() ) { return update_site_option( $option, $value ); } return update_option( $option, $value, $autoload ); } /** * Delete a setting. * * @param string $key */ public function remove_setting( string $key ) { $this->get_settings(); unset( $this->settings[ $key ] ); } /** * Removes a defined setting from the defined_settings array. * * Does not unset the actual constant. * * @param string $key */ public function remove_defined_setting( string $key ) { $this->get_defined_settings(); unset( $this->defined_settings[ $key ] ); } /** * Helper for displaying boolean settings. * * @param string $key setting key * * @return string */ public function on_off( string $key ): string { $value = $this->get_setting( $key, false ); return true === $value ? 'On' : 'Off'; } /** * Getter for $saving_settings. * * @return bool */ public function saving_settings(): bool { return $this->saving_settings; } /** * Setter for $saving_settings. * * @param bool $saving */ public function set_saving_settings( bool $saving ) { $this->saving_settings = $saving; } }