d_settings_error' ) ) { add_settings_error( // Slug title of the setting. $this->group_name, // Suffix-ID for the error message box. WordPress prepends `setting-error-`. $key, sprintf( /* translators: %s expands to an invalid URL. */ __( '%s does not seem to be a valid url. Please correct.', 'wordpress-seo' ), '' . esc_html( $submitted_url ) . '' ), // CSS class for the WP notice. 'notice-error' ); } // Restore the previous URL value, if any. if ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) { $url = WPSEO_Utils::sanitize_url( $old[ $key ] ); if ( $url !== '' ) { $clean[ $key ] = $url; } } Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $submitted_url ); return; } // The URL format is valid, let's sanitize it. $url = WPSEO_Utils::sanitize_url( $validated_url ); if ( $url !== '' ) { $clean[ $key ] = $url; } } } /** * Validates a Facebook App ID. * * @param string $key Key to check, in this case: the Facebook App ID field name. * @param array $dirty Dirty data with the new values. * @param array $old Old data. * @param array $clean Clean data by reference, normally the default values. */ public function validate_facebook_app_id( $key, $dirty, $old, &$clean ) { if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) { $url = 'https://graph.facebook.com/' . $dirty[ $key ]; $response = wp_remote_get( $url ); // These filters are used in the tests. /** * Filter: 'validate_facebook_app_id_api_response_code' - Allows to filter the Faceboook API response code. * * @api int $response_code The Facebook API response header code. */ $response_code = apply_filters( 'validate_facebook_app_id_api_response_code', wp_remote_retrieve_response_code( $response ) ); /** * Filter: 'validate_facebook_app_id_api_response_body' - Allows to filter the Faceboook API response body. * * @api string $response_body The Facebook API JSON response body. */ $response_body = apply_filters( 'validate_facebook_app_id_api_response_body', wp_remote_retrieve_body( $response ) ); $response_object = json_decode( $response_body ); /* * When the request is successful the response code will be 200 and * the response object will contain an `id` property. */ if ( $response_code === 200 && isset( $response_object->id ) ) { $clean[ $key ] = $dirty[ $key ]; return; } // Restore the previous value, if any. if ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) { $clean[ $key ] = $old[ $key ]; } if ( function_exists( 'add_settings_error' ) ) { add_settings_error( $this->group_name, // Slug title of the setting. $key, // Suffix-ID for the error message box. WordPress prepends `setting-error-`. sprintf( /* translators: %s expands to an invalid Facebook App ID. */ __( '%s does not seem to be a valid Facebook App ID. Please correct.', 'wordpress-seo' ), '' . esc_html( $dirty[ $key ] ) . '' ), // The error message. 'notice-error' // CSS class for the WP notice, either the legacy 'error' / 'updated' or the new `notice-*` ones. ); } Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $dirty[ $key ] ); } } /** * Remove the default filters. * Called from the validate() method to prevent failure to add new options. * * @return void */ public function remove_default_filters() { remove_filter( 'default_option_' . $this->option_name, array( $this, 'get_defaults' ) ); } /** * Get the enriched default value for an option. * * Checks if the concrete class contains an enrich_defaults() method and if so, runs it. * * {@internal The enrich_defaults method is used to set defaults for variable array keys * in an option, such as array keys depending on post_types and/or taxonomies.}} * * @return array */ public function get_defaults() { if ( method_exists( $this, 'translate_defaults' ) ) { $this->translate_defaults(); } if ( method_exists( $this, 'enrich_defaults' ) ) { $this->enrich_defaults(); } return apply_filters( 'wpseo_defaults', $this->defaults, $this->option_name ); } /** * Add filters to make sure that the option is merged with its defaults before being returned. * * @return void */ public function add_option_filters() { // Don't change, needs to check for false as could return prio 0 which would evaluate to false. if ( has_filter( 'option_' . $this->option_name, array( $this, 'get_option' ) ) === false ) { add_filter( 'option_' . $this->option_name, array( $this, 'get_option' ) ); } } /** * Remove the option filters. * Called from the clean_up methods to make sure we retrieve the original old option. * * @return void */ public function remove_option_filters() { remove_filter( 'option_' . $this->option_name, array( $this, 'get_option' ) ); } /** * Merge an option with its default values. * * This method should *not* be called directly!!! It is only meant to filter the get_option() results. * * @param mixed $options Option value. * * @return mixed Option merged with the defaults for that option. */ public function get_option( $options = null ) { $filtered = $this->array_filter_merge( $options ); /* * If the option contains variable option keys, make sure we don't remove those settings * - even if the defaults are not complete yet. * Unfortunately this means we also won't be removing the settings for post types or taxonomies * which are no longer in the WP install, but rather that than the other way around. */ if ( isset( $this->variable_array_key_patterns ) ) { $filtered = $this->retain_variable_keys( $options, $filtered ); } return $filtered; } /* *********** METHODS influencing add_uption(), update_option() and saving from admin pages. *********** */ /** * Register (whitelist) the option for the configuration pages. * The validation callback is already registered separately on the sanitize_option hook, * so no need to double register. * * @return void */ public function register_setting() { if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) { return; } if ( $this->multisite_only === true ) { $network_settings_api = Yoast_Network_Settings_API::get(); if ( $network_settings_api->meets_requirements() ) { $network_settings_api->register_setting( $this->group_name, $this->option_name ); } return; } register_setting( $this->group_name, $this->option_name ); } /** * Validate the option * * @param mixed $option_value The unvalidated new value for the option. * * @return array Validated new value for the option. */ public function validate( $option_value ) { $clean = $this->get_defaults(); /* Return the defaults if the new value is empty. */ if ( ! is_array( $option_value ) || $option_value === array() ) { return $clean; } $option_value = array_map( array( 'WPSEO_Utils', 'trim_recursive' ), $option_value ); $old = $this->get_original_option(); if ( ! is_array( $old ) ) { $old = array(); } $old = array_merge( $clean, $old ); $clean = $this->validate_option( $option_value, $clean, $old ); // Prevent updates to variables that are disabled via the override option. $clean = $this->prevent_disabled_options_update( $clean, $old ); /* Retain the values for variable array keys even when the post type/taxonomy is not yet registered. */ if ( isset( $this->variable_array_key_patterns ) ) { $clean = $this->retain_variable_keys( $option_value, $clean ); } $this->remove_default_filters(); return $clean; } /** * Checks whether a specific option key is disabled. * * This is determined by whether an override option is available with a key that equals the given key prefixed * with 'allow_'. * * @param string $key Option key. * * @return bool True if option key is disabled, false otherwise. */ public function is_disabled( $key ) { $override_option = $this->get_override_option(); if ( empty( $override_option ) ) { return false; } return isset( $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) && ! $override_option[ self::ALLOW_KEY_PREFIX . $key ]; } /** * All concrete classes must contain a validate_option() method which validates all * values within the option. * * @param array $dirty New value for the option. * @param array $clean Clean value for the option, normally the defaults. * @param array $old Old value of the option. */ abstract protected function validate_option( $dirty, $clean, $old ); /* *********** METHODS for ADDING/UPDATING/UPGRADING the option. *********** */ /** * Retrieve the real old value (unmerged with defaults). * * @return array|bool The original option value (which can be false if the option doesn't exist). */ protected function get_original_option() { $this->remove_default_filters(); $this->remove_option_filters(); // Get (unvalidated) array, NOT merged with defaults. if ( $this->multisite_only !== true ) { $option_value = get_option( $this->option_name ); } else { $option_value = get_site_option( $this->option_name ); } $this->add_option_filters(); $this->add_default_filters(); return $option_value; } /** * Add the option if it doesn't exist for some strange reason. * * @uses WPSEO_Option::get_original_option() * * @return void */ public function maybe_add_option() { if ( $this->get_original_option() === false ) { if ( $this->multisite_only !== true ) { update_option( $this->option_name, $this->get_defaults() ); } else { $this->update_site_option( $this->get_defaults() ); } } } /** * Update a site_option. * * {@internal This special method is only needed for multisite options, but very needed indeed there. * The order in which certain functions and hooks are run is different between * get_option() and get_site_option() which means in practice that the removing * of the default filters would be done too late and the re-adding of the default * filters might not be done at all. * Aka: use the WPSEO_Options::update_site_option() method (which calls this method) * for safely adding/updating multisite options.}} * * @param mixed $value The new value for the option. * * @return bool Whether the update was succesfull. */ public function update_site_option( $value ) { if ( $this->multisite_only === true && is_multisite() ) { $this->remove_default_filters(); $result = update_site_option( $this->option_name, $value ); $this->add_default_filters(); return $result; } else { return false; } } /** * Retrieve the real old value (unmerged with defaults), clean and re-save the option. * * @uses WPSEO_Option::get_original_option() * @uses WPSEO_Option::import() * * @param string $current_version Optional. Version from which to upgrade, if not set, * version specific upgrades will be disregarded. * * @return void */ public function clean( $current_version = null ) { $option_value = $this->get_original_option(); $this->import( $option_value, $current_version ); } /** * Clean and re-save the option. * * @uses clean_option() method from concrete class if it exists. * * @todo [JRF/whomever] Figure out a way to show settings error during/after the upgrade - maybe * something along the lines of: * -> add them to a property in this class * -> if that property isset at the end of the routine and add_settings_error function does not exist, * save as transient (or update the transient if one already exists) * -> next time an admin is in the WP back-end, show the errors and delete the transient or only delete it * once the admin has dismissed the message (add ajax function) * Important: all validation routines which add_settings_errors would need to be changed for this to work * * @param array $option_value Option value to be imported. * @param string $current_version Optional. Version from which to upgrade, if not set, * version specific upgrades will be disregarded. * @param array $all_old_option_values Optional. Only used when importing old options to * have access to the real old values, in contrast to * the saved ones. * * @return void */ public function import( $option_value, $current_version = null, $all_old_option_values = null ) { if ( $option_value === false ) { $option_value = $this->get_defaults(); } elseif ( is_array( $option_value ) && method_exists( $this, 'clean_option' ) ) { $option_value = $this->clean_option( $option_value, $current_version, $all_old_option_values ); } /* * Save the cleaned value - validation will take care of cleaning out array keys which * should no longer be there. */ if ( $this->multisite_only !== true ) { update_option( $this->option_name, $option_value ); } else { $this->update_site_option( $this->option_name, $option_value ); } } /** * Returns the variable array key patterns for an options class. * * @return array */ public function get_patterns() { return (array) $this->variable_array_key_patterns; } /** * Concrete classes *may* contain a clean_option method which will clean out old/renamed * values within the option. */ // abstract public function clean_option( $option_value, $current_version = null, $all_old_option_values = null ); /* *********** HELPER METHODS for internal use. *********** */ /** * Helper method - Combines a fixed array of default values with an options array * while filtering out any keys which are not in the defaults array. * * @todo [JRF] - shouldn't this be a straight array merge ? at the end of the day, the validation * removes any invalid keys on save. * * @param array $options Optional. Current options. If not set, the option defaults * for the $option_key will be returned. * * @return array Combined and filtered options array. */ protected function array_filter_merge( $options = null ) { $defaults = $this->get_defaults(); if ( ! isset( $options ) || $options === false || $options === array() ) { return $defaults; } $options = (array) $options; /* $filtered = array(); if ( $defaults !== array() ) { foreach ( $defaults as $key => $default_value ) { // @todo should this walk through array subkeys ? $filtered[ $key ] = ( isset( $options[ $key ] ) ? $options[ $key ] : $default_value ); } } */ $filtered = array_merge( $defaults, $options ); return $filtered; } /** * Sets updated values for variables that are disabled via the override option back to their previous values. * * @param array $updated Updated option value. * @param array $old Old option value. * * @return array Updated option value, with all disabled variables set to their old values. */ protected function prevent_disabled_options_update( $updated, $old ) { $override_option = $this->get_override_option(); if ( empty( $override_option ) ) { return $updated; } /* * This loop could as well call `is_disabled( $key )` for each iteration, * however this would be worse performance-wise. */ foreach ( $old as $key => $value ) { if ( isset( $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) && ! $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) { $updated[ $key ] = $old[ $key ]; } } return $updated; } /** * Retrieves the value of the override option, if available. * * An override option contains values that may determine access to certain sub-variables * of this option. * * Only regular options in multisite can have override options, which in that case * would be network options. * * @return array Override option value, or empty array if unavailable. */ protected function get_override_option() { if ( empty( $this->override_option_name ) || $this->multisite_only === true || ! is_multisite() ) { return array(); } return get_site_option( $this->override_option_name, array() ); } /** * Make sure that any set option values relating to post_types and/or taxonomies are retained, * even when that post_type or taxonomy may not yet have been registered. * * {@internal The wpseo_titles concrete class overrules this method. Make sure that any * changes applied here, also get ported to that version.}} * * @param array $dirty Original option as retrieved from the database. * @param array $clean Filtered option where any options which shouldn't be in our option * have already been removed and any options which weren't set * have been set to their defaults. * * @return array */ protected function retain_variable_keys( $dirty, $clean ) { if ( ( is_array( $this->variable_array_key_patterns ) && $this->variable_array_key_patterns !== array() ) && ( is_array( $dirty ) && $dirty !== array() ) ) { foreach ( $dirty as $key => $value ) { // Do nothing if already in filtered options. if ( isset( $clean[ $key ] ) ) { continue; } foreach ( $this->variable_array_key_patterns as $pattern ) { if ( strpos( $key, $pattern ) === 0 ) { $clean[ $key ] = $value; break; } } } } return $clean; } /** * Check whether a given array key conforms to one of the variable array key patterns for this option. * * @usedby validate_option() methods for options with variable array keys. * * @param string $key Array key to check. * * @return string Pattern if it conforms, original array key if it doesn't or if the option * does not have variable array keys. */ protected function get_switch_key( $key ) { if ( ! isset( $this->variable_array_key_patterns ) || ( ! is_array( $this->variable_array_key_patterns ) || $this->variable_array_key_patterns === array() ) ) { return $key; } foreach ( $this->variable_array_key_patterns as $pattern ) { if ( strpos( $key, $pattern ) === 0 ) { return $pattern; } } return $key; } }