imeout = HOUR_IN_SECONDS;
break;
default:
if ( $doing_cron ) {
$timeout = 2 * HOUR_IN_SECONDS;
} else {
$timeout = 12 * HOUR_IN_SECONDS;
}
}
$time_not_changed = isset( $last_update->last_checked ) && $timeout > ( time() - $last_update->last_checked );
if ( $time_not_changed && ! $extra_stats ) {
$theme_changed = false;
foreach ( $checked as $slug => $v ) {
if ( ! isset( $last_update->checked[ $slug ] ) || (string) $last_update->checked[ $slug ] !== (string) $v ) {
$theme_changed = true;
}
}
if ( isset( $last_update->response ) && is_array( $last_update->response ) ) {
foreach ( $last_update->response as $slug => $update_details ) {
if ( ! isset( $checked[ $slug ] ) ) {
$theme_changed = true;
break;
}
}
}
// Bail if we've checked recently and if nothing has changed.
if ( ! $theme_changed ) {
return;
}
}
// Update last_checked for current to prevent multiple blocking requests if request hangs.
$last_update->last_checked = time();
set_site_transient( 'update_themes', $last_update );
$request['themes'] = $themes;
$locales = array_values( get_available_languages() );
/**
* Filters the locales requested for theme translations.
*
* @since 3.7.0
* @since 4.5.0 The default value of the `$locales` parameter changed to include all locales.
*
* @param string[] $locales Theme locales. Default is all available locales of the site.
*/
$locales = apply_filters( 'themes_update_check_locales', $locales );
$locales = array_unique( $locales );
if ( $doing_cron ) {
$timeout = 30; // 30 seconds.
} else {
// Three seconds, plus one extra second for every 10 themes.
$timeout = 3 + (int) ( count( $themes ) / 10 );
}
$options = array(
'timeout' => $timeout,
'body' => array(
'themes' => wp_json_encode( $request ),
'translations' => wp_json_encode( $translations ),
'locale' => wp_json_encode( $locales ),
),
'user-agent' => 'WordPress/' . $wp_version . '; ' . home_url( '/' ),
);
if ( $extra_stats ) {
$options['body']['update_stats'] = wp_json_encode( $extra_stats );
}
$url = 'http://api.wordpress.org/themes/update-check/1.1/';
$http_url = $url;
$ssl = wp_http_supports( array( 'ssl' ) );
if ( $ssl ) {
$url = set_url_scheme( $url, 'https' );
}
$raw_response = wp_remote_post( $url, $options );
if ( $ssl && is_wp_error( $raw_response ) ) {
trigger_error(
sprintf(
/* translators: %s: Support forums URL. */
__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server’s configuration. If you continue to have problems, please try the support forums.' ),
__( 'https://wordpress.org/support/forums/' )
) . ' ' . __( '(WordPress could not establish a secure connection to WordPress.org. Please contact your server administrator.)' ),
headers_sent() || WP_DEBUG ? E_USER_WARNING : E_USER_NOTICE
);
$raw_response = wp_remote_post( $http_url, $options );
}
if ( is_wp_error( $raw_response ) || 200 !== wp_remote_retrieve_response_code( $raw_response ) ) {
return;
}
$new_update = new stdClass();
$new_update->last_checked = time();
$new_update->checked = $checked;
$response = json_decode( wp_remote_retrieve_body( $raw_response ), true );
if ( is_array( $response ) ) {
$new_update->response = $response['themes'];
$new_update->no_update = $response['no_update'];
$new_update->translations = $response['translations'];
}
// Support updates for any themes using the `Update URI` header field.
foreach ( $themes as $theme_stylesheet => $theme_data ) {
if ( ! $theme_data['UpdateURI'] || isset( $new_update->response[ $theme_stylesheet ] ) ) {
continue;
}
$hostname = wp_parse_url( sanitize_url( $theme_data['UpdateURI'] ), PHP_URL_HOST );
/**
* Filters the update response for a given theme hostname.
*
* The dynamic portion of the hook name, `$hostname`, refers to the hostname
* of the URI specified in the `Update URI` header field.
*
* @since 6.1.0
*
* @param array|false $update {
* The theme update data with the latest details. Default false.
*
* @type string $id Optional. ID of the theme for update purposes, should be a URI
* specified in the `Update URI` header field.
* @type string $theme Directory name of the theme.
* @type string $version The version of the theme.
* @type string $url The URL for details of the theme.
* @type string $package Optional. The update ZIP for the theme.
* @type string $tested Optional. The version of WordPress the theme is tested against.
* @type string $requires_php Optional. The version of PHP which the theme requires.
* @type bool $autoupdate Optional. Whether the theme should automatically update.
* @type array $translations {
* Optional. List of translation updates for the theme.
*
* @type string $language The language the translation update is for.
* @type string $version The version of the theme this translation is for.
* This is not the version of the language file.
* @type string $updated The update timestamp of the translation file.
* Should be a date in the `YYYY-MM-DD HH:MM:SS` format.
* @type string $package The ZIP location containing the translation update.
* @type string $autoupdate Whether the translation should be automatically installed.
* }
* }
* @param array $theme_data Theme headers.
* @param string $theme_stylesheet Theme stylesheet.
* @param string[] $locales Installed locales to look up translations for.
*/
$update = apply_filters( "update_themes_{$hostname}", false, $theme_data, $theme_stylesheet, $locales );
if ( ! $update ) {
continue;
}
$update = (object) $update;
// Is it valid? We require at least a version.
if ( ! isset( $update->version ) ) {
continue;
}
// This should remain constant.
$update->id = $theme_data['UpdateURI'];
// WordPress needs the version field specified as 'new_version'.
if ( ! isset( $update->new_version ) ) {
$update->new_version = $update->version;
}
// Handle any translation updates.
if ( ! empty( $update->translations ) ) {
foreach ( $update->translations as $translation ) {
if ( isset( $translation['language'], $translation['package'] ) ) {
$translation['type'] = 'theme';
$translation['slug'] = isset( $update->theme ) ? $update->theme : $update->id;
$new_update->translations[] = $translation;
}
}
}
unset( $new_update->no_update[ $theme_stylesheet ], $new_update->response[ $theme_stylesheet ] );
if ( version_compare( $update->new_version, $theme_data['Version'], '>' ) ) {
$new_update->response[ $theme_stylesheet ] = (array) $update;
} else {
$new_update->no_update[ $theme_stylesheet ] = (array) $update;
}
}
set_site_transient( 'update_themes', $new_update );
}
/**
* Performs WordPress automatic background updates.
*
* Updates WordPress core plus any plugins and themes that have automatic updates enabled.
*
* @since 3.7.0
*/
function wp_maybe_auto_update() {
require_once ABSPATH . 'wp-admin/includes/admin.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
$upgrader = new WP_Automatic_Updater();
$upgrader->run();
}
/**
* Retrieves a list of all language updates available.
*
* @since 3.7.0
*
* @return object[] Array of translation objects that have available updates.
*/
function wp_get_translation_updates() {
$updates = array();
$transients = array(
'update_core' => 'core',
'update_plugins' => 'plugin',
'update_themes' => 'theme',
);
foreach ( $transients as $transient => $type ) {
$transient = get_site_transient( $transient );
if ( empty( $transient->translations ) ) {
continue;
}
foreach ( $transient->translations as $translation ) {
$updates[] = (object) $translation;
}
}
return $updates;
}
/**
* Collects counts and UI strings for available updates.
*
* @since 3.3.0
*
* @return array
*/
function wp_get_update_data() {
$counts = array(
'plugins' => 0,
'themes' => 0,
'wordpress' => 0,
'translations' => 0,
);
$plugins = current_user_can( 'update_plugins' );
if ( $plugins ) {
$update_plugins = get_site_transient( 'update_plugins' );
if ( ! empty( $update_plugins->response ) ) {
$counts['plugins'] = count( $update_plugins->response );
}
}
$themes = current_user_can( 'update_themes' );
if ( $themes ) {
$update_themes = get_site_transient( 'update_themes' );
if ( ! empty( $update_themes->response ) ) {
$counts['themes'] = count( $update_themes->response );
}
}
$core = current_user_can( 'update_core' );
if ( $core && function_exists( 'get_core_updates' ) ) {
$update_wordpress = get_core_updates( array( 'dismissed' => false ) );
if ( ! empty( $update_wordpress )
&& ! in_array( $update_wordpress[0]->response, array( 'development', 'latest' ), true )
&& current_user_can( 'update_core' )
) {
$counts['wordpress'] = 1;
}
}
if ( ( $core || $plugins || $themes ) && wp_get_translation_updates() ) {
$counts['translations'] = 1;
}
$counts['total'] = $counts['plugins'] + $counts['themes'] + $counts['wordpress'] + $counts['translations'];
$titles = array();
if ( $counts['wordpress'] ) {
/* translators: %d: Number of available WordPress updates. */
$titles['wordpress'] = sprintf( __( '%d WordPress Update' ), $counts['wordpress'] );
}
if ( $counts['plugins'] ) {
/* translators: %d: Number of available plugin updates. */
$titles['plugins'] = sprintf( _n( '%d Plugin Update', '%d Plugin Updates', $counts['plugins'] ), $counts['plugins'] );
}
if ( $counts['themes'] ) {
/* translators: %d: Number of available theme updates. */
$titles['themes'] = sprintf( _n( '%d Theme Update', '%d Theme Updates', $counts['themes'] ), $counts['themes'] );
}
if ( $counts['translations'] ) {
$titles['translations'] = __( 'Translation Updates' );
}
$update_title = $titles ? esc_attr( implode( ', ', $titles ) ) : '';
$update_data = array(
'counts' => $counts,
'title' => $update_title,
);
/**
* Filters the returned array of update data for plugins, themes, and WordPress core.
*
* @since 3.5.0
*
* @param array $update_data {
* Fetched update data.
*
* @type array $counts An array of counts for available plugin, theme, and WordPress updates.
* @type string $update_title Titles of available updates.
* }
* @param array $titles An array of update counts and UI strings for available updates.
*/
return apply_filters( 'wp_get_update_data', $update_data, $titles );
}
/**
* Determines whether core should be updated.
*
* @since 2.8.0
*
* @global string $wp_version The WordPress version string.
*/
function _maybe_update_core() {
// Include an unmodified $wp_version.
require ABSPATH . WPINC . '/version.php';
$current = get_site_transient( 'update_core' );
if ( isset( $current->last_checked, $current->version_checked )
&& 12 * HOUR_IN_SECONDS > ( time() - $current->last_checked )
&& $current->version_checked === $wp_version
) {
return;
}
wp_version_check();
}
/**
* Checks the last time plugins were run before checking plugin versions.
*
* This might have been backported to WordPress 2.6.1 for performance reasons.
* This is used for the wp-admin to check only so often instead of every page
* load.
*
* @since 2.7.0
* @access private
*/
function _maybe_update_plugins() {
$current = get_site_transient( 'update_plugins' );
if ( isset( $current->last_checked )
&& 12 * HOUR_IN_SECONDS > ( time() - $current->last_checked )
) {
return;
}
wp_update_plugins();
}
/**
* Checks themes versions only after a duration of time.
*
* This is for performance reasons to make sure that on the theme version
* checker is not run on every page load.
*
* @since 2.7.0
* @access private
*/
function _maybe_update_themes() {
$current = get_site_transient( 'update_themes' );
if ( isset( $current->last_checked )
&& 12 * HOUR_IN_SECONDS > ( time() - $current->last_checked )
) {
return;
}
wp_update_themes();
}
/**
* Schedules core, theme, and plugin update checks.
*
* @since 3.1.0
*/
function wp_schedule_update_checks() {
if ( ! wp_next_scheduled( 'wp_version_check' ) && ! wp_installing() ) {
wp_schedule_event( time(), 'twicedaily', 'wp_version_check' );
}
if ( ! wp_next_scheduled( 'wp_update_plugins' ) && ! wp_installing() ) {
wp_schedule_event( time(), 'twicedaily', 'wp_update_plugins' );
}
if ( ! wp_next_scheduled( 'wp_update_themes' ) && ! wp_installing() ) {
wp_schedule_event( time(), 'twicedaily', 'wp_update_themes' );
}
}
/**
* Clears existing update caches for plugins, themes, and core.
*
* @since 4.1.0
*/
function wp_clean_update_cache() {
if ( function_exists( 'wp_clean_plugins_cache' ) ) {
wp_clean_plugins_cache();
} else {
delete_site_transient( 'update_plugins' );
}
wp_clean_themes_cache();
delete_site_transient( 'update_core' );
}
/**
* Schedules the removal of all contents in the temporary backup directory.
*
* @since 6.3.0
*/
function wp_delete_all_temp_backups() {
/*
* Check if there is a lock, or if currently performing an Ajax request,
* in which case there is a chance an update is running.
* Reschedule for an hour from now and exit early.
*/
if ( get_option( 'core_updater.lock' ) || get_option( 'auto_updater.lock' ) || wp_doing_ajax() ) {
wp_schedule_single_event( time() + HOUR_IN_SECONDS, 'wp_delete_temp_updater_backups' );
return;
}
// This action runs on shutdown to make sure there are no plugin updates currently running.
add_action( 'shutdown', '_wp_delete_all_temp_backups' );
}
/**
* Deletes all contents in the temporary backup directory.
*
* @since 6.3.0
*
* @access private
*
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
*
* @return void|WP_Error Void on success, or a WP_Error object on failure.
*/
function _wp_delete_all_temp_backups() {
global $wp_filesystem;
if ( ! function_exists( 'WP_Filesystem' ) ) {
require_once ABSPATH . '/wp-admin/includes/file.php';
}
ob_start();
$credentials = request_filesystem_credentials( '' );
ob_end_clean();
if ( false === $credentials || ! WP_Filesystem( $credentials ) ) {
return new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.' ) );
}
if ( ! $wp_filesystem->wp_content_dir() ) {
return new WP_Error(
'fs_no_content_dir',
/* translators: %s: Directory name. */
sprintf( __( 'Unable to locate WordPress content directory (%s).' ), 'wp-content' )
);
}
$temp_backup_dir = $wp_filesystem->wp_content_dir() . 'upgrade-temp-backup/';
$dirlist = $wp_filesystem->dirlist( $temp_backup_dir );
$dirlist = $dirlist ? $dirlist : array();
foreach ( array_keys( $dirlist ) as $dir ) {
if ( '.' === $dir || '..' === $dir ) {
continue;
}
$wp_filesystem->delete( $temp_backup_dir . $dir, true );
}
}
if ( ( ! is_main_site() && ! is_network_admin() ) || wp_doing_ajax() ) {
return;
}
add_action( 'admin_init', '_maybe_update_core' );
add_action( 'wp_version_check', 'wp_version_check' );
add_action( 'load-plugins.php', 'wp_update_plugins' );
add_action( 'load-update.php', 'wp_update_plugins' );
add_action( 'load-update-core.php', 'wp_update_plugins' );
add_action( 'admin_init', '_maybe_update_plugins' );
add_action( 'wp_update_plugins', 'wp_update_plugins' );
add_action( 'load-themes.php', 'wp_update_themes' );
add_action( 'load-update.php', 'wp_update_themes' );
add_action( 'load-update-core.php', 'wp_update_themes' );
add_action( 'admin_init', '_maybe_update_themes' );
add_action( 'wp_update_themes', 'wp_update_themes' );
add_action( 'update_option_WPLANG', 'wp_clean_update_cache', 10, 0 );
add_action( 'wp_maybe_auto_update', 'wp_maybe_auto_update' );
add_action( 'init', 'wp_schedule_update_checks' );
add_action( 'wp_delete_temp_updater_backups', 'wp_delete_all_temp_backups' );
/
public $group_name;
/**
* Whether to include the option in the return for WPSEO_Options::get_all().
*
* Also determines which options are copied over for ms_(re)set_blog().
*
* @var bool
*/
public $include_in_all = true;
/**
* Whether this option is only for when the install is multisite.
*
* @var bool
*/
public $multisite_only = false;
/**
* Array of defaults for the option - MUST be set in concrete class.
*
* Shouldn't be requested directly, use $this->get_defaults();
*
* @var array
*/
protected $defaults;
/**
* Array of variable option name patterns for the option - if any.
*
* Set this when the option contains array keys which vary based on post_type
* or taxonomy.
*
* @var array
*/
protected $variable_array_key_patterns;
/**
* Array of sub-options which should not be overloaded with multi-site defaults.
*
* @var array
*/
public $ms_exclude = [];
/**
* Name for an option higher in the hierarchy to override setting access.
*
* @var string
*/
protected $override_option_name;
/**
* Instance of this class.
*
* @var WPSEO_Option
*/
protected static $instance;
/* *********** INSTANTIATION METHODS *********** */
/**
* Add all the actions and filters for the option.
*/
protected function __construct() {
/* Add filters which get applied to the get_options() results. */
$this->add_default_filters(); // Return defaults if option not set.
$this->add_option_filters(); // Merge with defaults if option *is* set.
if ( $this->multisite_only !== true ) {
/**
* The option validation routines remove the default filters to prevent failing
* to insert an option if it's new. Let's add them back afterwards.
*/
add_action( 'add_option', [ $this, 'add_default_filters_if_same_option' ] ); // Adding back after INSERT.
add_action( 'update_option', [ $this, 'add_default_filters_if_same_option' ] );
add_filter( 'pre_update_option', [ $this, 'add_default_filters_if_not_changed' ], PHP_INT_MAX, 3 );
// Refills the cache when the option has been updated.
add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Options', 'clear_cache' ], 10 );
}
elseif ( is_multisite() ) {
/*
* The option validation routines remove the default filters to prevent failing
* to insert an option if it's new. Let's add them back afterwards.
*
* For site_options, this method is not foolproof as these actions are not fired
* on an insert/update failure. Please use the WPSEO_Options::update_site_option() method
* for updating site options to make sure the filters are in place.
*/
add_action( 'add_site_option_' . $this->option_name, [ $this, 'add_default_filters' ] );
add_action( 'update_site_option_' . $this->option_name, [ $this, 'add_default_filters' ] );
add_filter( 'pre_update_site_option_' . $this->option_name, [ $this, 'add_default_filters_if_not_changed' ], PHP_INT_MAX, 3 );
// Refills the cache when the option has been updated.
add_action( 'update_site_option_' . $this->option_name, [ 'WPSEO_Options', 'clear_cache' ], 1, 0 );
}
/*
* Make sure the option will always get validated, independently of register_setting()
* (only available on back-end).
*/
add_filter( 'sanitize_option_' . $this->option_name, [ $this, 'validate' ] );
/* Register our option for the admin pages */
add_action( 'admin_init', [ $this, 'register_setting' ] );
/* Set option group name if not given */
if ( ! isset( $this->group_name ) || $this->group_name === '' ) {
$this->group_name = 'yoast_' . $this->option_name . '_options';
}
/* Translate some defaults as early as possible - textdomain is loaded in init on priority 1. */
if ( method_exists( $this, 'translate_defaults' ) ) {
add_action( 'init', [ $this, 'translate_defaults' ], 2 );
}
/**
* Enrich defaults once custom post types and taxonomies have been registered
* which is normally done on the init action.
*
* @todo [JRF/testers] Verify that none of the options which are only available after
* enrichment are used before the enriching.
*/
if ( method_exists( $this, 'enrich_defaults' ) ) {
add_action( 'init', [ $this, 'enrich_defaults' ], 99 );
}
}
/*
* All concrete classes *must* contain the get_instance method.
*
* {@internal Unfortunately I can't define it as an abstract as it also *has* to be static...}}
*
* ```
* abstract protected static function get_instance();
* ```
* ---------------
*
* Concrete classes *may* contain a translate_defaults method.
* ```
* abstract public function translate_defaults();
* ```
* ---------------
*
* Concrete classes *may* contain an enrich_defaults method to add additional defaults once
* all post_types and taxonomies have been registered.
*
* ```
* abstract public function enrich_defaults();
* ```
*/
/* *********** METHODS INFLUENCING get_option() *********** */
/**
* Add filters to make sure that the option default is returned if the option is not set.
*
* @return void
*/
public function add_default_filters() {
// Don't change, needs to check for false as could return prio 0 which would evaluate to false.
if ( has_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] ) === false ) {
add_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] );
}
}
/**
* Adds back the default filters that were removed during validation if the option was changed.
* Checks if this option was changed to prevent constantly checking if filters are present.
*
* @param string $option_name The option name.
*
* @return void
*/
public function add_default_filters_if_same_option( $option_name ) {
if ( $option_name === $this->option_name ) {
$this->add_default_filters();
}
}
/**
* Adds back the default filters that were removed during validation if the option was not changed.
* This is because in that case the latter actions are not called and thus the filters are never
* added back.
*
* @param mixed $value The current value.
* @param string $option_name The option name.
* @param mixed $old_value The old value.
*
* @return string The current value.
*/
public function add_default_filters_if_not_changed( $value, $option_name, $old_value ) {
if ( $option_name !== $this->option_name ) {
return $value;
}
if ( $value === $old_value || maybe_serialize( $value ) === maybe_serialize( $old_value ) ) {
$this->add_default_filters();
}
return $value;
}
/**
* Validate webmaster tools & Pinterest verification strings.
*
* @param string $key Key to check, by type of service.
* @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.
*
* @return void
*/
public function validate_verification_string( $key, $dirty, $old, &$clean ) {
if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
$meta = $dirty[ $key ];
if ( strpos( $meta, 'content=' ) ) {
// Make sure we only have the real key, not a complete meta tag.
preg_match( '`content=([\'"])?([^\'"> ]+)(?:\1|[ />])`', $meta, $match );
if ( isset( $match[2] ) ) {
$meta = $match[2];
}
unset( $match );
}
$meta = sanitize_text_field( $meta );
if ( $meta !== '' ) {
$regex = '`^[A-Fa-f0-9_-]+$`';
$service = '';
switch ( $key ) {
case 'baiduverify':
$regex = '`^[A-Za-z0-9_-]+$`';
$service = 'Baidu Webmaster tools';
break;
case 'googleverify':
$regex = '`^[A-Za-z0-9_-]+$`';
$service = 'Google Webmaster tools';
break;
case 'msverify':
$service = 'Bing Webmaster tools';
break;
case 'pinterestverify':
$service = 'Pinterest';
break;
case 'yandexverify':
$service = 'Yandex Webmaster tools';
break;
}
if ( preg_match( $regex, $meta ) ) {
$clean[ $key ] = $meta;
}
else {
// Restore the previous value, if any.
if ( isset( $old[ $key ] ) && preg_match( $regex, $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-`.
/* translators: 1: Verification string from user input; 2: Service name. */
sprintf( __( '%1$s does not seem to be a valid %2$s verification string. Please correct.', 'wordpress-seo' ), '' . esc_html( $meta ) . '', $service ), // The error message.
'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, $meta );
}
}
}
}
/**
* Validates an option as a valid URL. Prints out a WordPress settings error
* notice if the URL is invalid.
*
* @param string $key Key to check, by type of URL setting.
* @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.
*
* @return void
*/
public function validate_url( $key, $dirty, $old, &$clean ) {
if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
$submitted_url = trim( $dirty[ $key ] );
$validated_url = filter_var( WPSEO_Utils::sanitize_url( $submitted_url ), FILTER_VALIDATE_URL );
if ( $validated_url === false ) {
if ( function_exists( 'add_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,
// The error message.
sprintf(
/* translators: %s expands to an invalid URL. */
__( '%s does not seem to be a valid url. Please correct.', 'wordpress-seo' ),
'' . esc_url( $submitted_url ) . ''
),
// Message type.
'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;
}
}
}
/**
* 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, [ $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, [ $this, 'get_option' ] ) === false ) {
add_filter( 'option_' . $this->option_name, [ $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, [ $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_option(), 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 === [] ) {
return $clean;
}
$option_value = array_map( [ 'WPSEO_Utils', 'trim_recursive' ], $option_value );
$old = $this->get_original_option();
if ( ! is_array( $old ) ) {
$old = [];
}
$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 successful.
*/
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|null $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|null $current_version Optional. Version from which to upgrade, if not set,
* version-specific upgrades will be disregarded.
* @param array|null $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;
}
/**
* Retrieves the option name.
*
* @return string The set option name.
*/
public function get_option_name() {
return $this->option_name;
}
/*
* 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|null $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 === [] ) {
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 [];
}
return get_site_option( $this->override_option_name, [] );
}
/**
* 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 !== [] ) && ( is_array( $dirty ) && $dirty !== [] ) ) {
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.
*
* @used-by 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 === [] ) ) {
return $key;
}
foreach ( $this->variable_array_key_patterns as $pattern ) {
if ( strpos( $key, $pattern ) === 0 ) {
return $pattern;
}
}
return $key;
}
}