PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 18.3
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v18.3
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / inc / options / class-wpseo-option.php

class-wpseo-option.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 18.3, at inc/options/class-wpseo-option.php

966 lines 32.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WPSEO plugin file.
4 *
5 * @package WPSEO\Internals\Options
6 */
7
8 /**
9 * This abstract class and it's concrete classes implement defaults and value validation for
10 * all WPSEO options and subkeys within options.
11 *
12 * Some guidelines:
13 * [Retrieving options]
14 * - Use the normal get_option() to retrieve an option. You will receive a complete array for the option.
15 * Any subkeys which were not set, will have their default values in place.
16 * - In other words, you will normally not have to check whether a subkey isset() as they will *always* be set.
17 * They will also *always* be of the correct variable type.
18 * The only exception to this are the options with variable option names based on post_type or taxonomy
19 * as those will not always be available before the taxonomy/post_type is registered.
20 * (they will be available if a value was set, they won't be if it wasn't as the class won't know
21 * that a default needs to be injected).
22 *
23 * [Updating/Adding options]
24 * - For multisite site_options, please use the WPSEO_Options::update_site_option() method.
25 * - For normal options, use the normal add/update_option() functions. As long a the classes here
26 * are instantiated, validation for all options and their subkeys will be automatic.
27 * - On (succesfull) update of a couple of options, certain related actions will be run automatically.
28 * Some examples:
29 * - on change of wpseo[yoast_tracking], the cron schedule will be adjusted accordingly
30 * - on change of wpseo and wpseo_title, some caches will be cleared
31 *
32 * [Important information about add/updating/changing these classes]
33 * - Make sure that option array key names are unique across options. The WPSEO_Options::get_all()
34 * method merges most options together. If any of them have non-unique names, even if they
35 * are in a different option, they *will* overwrite each other.
36 * - When you add a new array key in an option: make sure you add proper defaults and add the key
37 * to the validation routine in the proper place or add a new validation case.
38 * You don't need to do any upgrading as any option returned will always be merged with the
39 * defaults, so new options will automatically be available.
40 * If the default value is a string which need translating, add this to the concrete class
41 * translate_defaults() method.
42 * - When you remove an array key from an option: if it's important that the option is really removed,
43 * add the WPSEO_Option::clean_up( $option_name ) method to the upgrade run.
44 * This will re-save the option and automatically remove the array key no longer in existance.
45 * - When you rename a sub-option: add it to the clean_option() routine and run that in the upgrade run.
46 * - When you change the default for an option sub-key, make sure you verify that the validation routine will
47 * still work the way it should.
48 * Example: changing a default from '' (empty string) to 'text' with a validation routine with tests
49 * for an empty string will prevent a user from saving an empty string as the real value. So the
50 * test for '' with the validation routine would have to be removed in that case.
51 * - If an option needs specific actions different from defined in this abstract class, you can just overrule
52 * a method by defining it in the concrete class.
53 *
54 * @todo [JRF => testers] Double check that validation will not cause errors when called
55 * from upgrade routine (some of the WP functions may not yet be available).
56 */
57 abstract class WPSEO_Option {
58
59 /**
60 * Prefix for override option keys that allow or disallow the option key of the same name.
61 *
62 * @var string
63 */
64 const ALLOW_KEY_PREFIX = 'allow_';
65
66 /**
67 * Option name - MUST be set in concrete class and set to public.
68 *
69 * @var string
70 */
71 protected $option_name;
72
73 /**
74 * Option group name for use in settings forms.
75 *
76 * Will be set automagically if not set in concrete class (i.e.
77 * if it confirm to the normal pattern 'yoast' . $option_name . 'options',
78 * only set in conrete class if it doesn't).
79 *
80 * @var string
81 */
82 public $group_name;
83
84 /**
85 * Whether to include the option in the return for WPSEO_Options::get_all().
86 *
87 * Also determines which options are copied over for ms_(re)set_blog().
88 *
89 * @var bool
90 */
91 public $include_in_all = true;
92
93 /**
94 * Whether this option is only for when the install is multisite.
95 *
96 * @var bool
97 */
98 public $multisite_only = false;
99
100 /**
101 * Array of defaults for the option - MUST be set in concrete class.
102 *
103 * Shouldn't be requested directly, use $this->get_defaults();
104 *
105 * @var array
106 */
107 protected $defaults;
108
109 /**
110 * Array of variable option name patterns for the option - if any -.
111 *
112 * Set this when the option contains array keys which vary based on post_type
113 * or taxonomy.
114 *
115 * @var array
116 */
117 protected $variable_array_key_patterns;
118
119 /**
120 * Array of sub-options which should not be overloaded with multi-site defaults.
121 *
122 * @var array
123 */
124 public $ms_exclude = [];
125
126 /**
127 * Name for an option higher in the hierarchy to override setting access.
128 *
129 * @var string
130 */
131 protected $override_option_name;
132
133 /**
134 * Instance of this class.
135 *
136 * @var WPSEO_Option
137 */
138 protected static $instance;
139
140
141 /* *********** INSTANTIATION METHODS *********** */
142
143 /**
144 * Add all the actions and filters for the option.
145 */
146 protected function __construct() {
147
148 /* Add filters which get applied to the get_options() results. */
149 $this->add_default_filters(); // Return defaults if option not set.
150 $this->add_option_filters(); // Merge with defaults if option *is* set.
151
152
153 if ( $this->multisite_only !== true ) {
154 /**
155 * The option validation routines remove the default filters to prevent failing
156 * to insert an option if it's new. Let's add them back afterwards.
157 */
158 add_action( 'add_option', [ $this, 'add_default_filters_if_same_option' ] ); // Adding back after INSERT.
159
160 add_action( 'update_option', [ $this, 'add_default_filters_if_same_option' ] );
161
162 add_filter( 'pre_update_option', [ $this, 'add_default_filters_if_not_changed' ], PHP_INT_MAX, 3 );
163
164 // Refills the cache when the option has been updated.
165 add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Options', 'clear_cache' ], 10 );
166 }
167 elseif ( is_multisite() ) {
168 /*
169 * The option validation routines remove the default filters to prevent failing
170 * to insert an option if it's new. Let's add them back afterwards.
171 *
172 * For site_options, this method is not foolproof as these actions are not fired
173 * on an insert/update failure. Please use the WPSEO_Options::update_site_option() method
174 * for updating site options to make sure the filters are in place.
175 */
176 add_action( 'add_site_option_' . $this->option_name, [ $this, 'add_default_filters' ] );
177 add_action( 'update_site_option_' . $this->option_name, [ $this, 'add_default_filters' ] );
178 add_filter( 'pre_update_site_option_' . $this->option_name, [ $this, 'add_default_filters_if_not_changed' ], PHP_INT_MAX, 3 );
179
180 // Refills the cache when the option has been updated.
181 add_action( 'update_site_option_' . $this->option_name, [ 'WPSEO_Options', 'clear_cache' ], 1, 0 );
182 }
183
184
185 /*
186 * Make sure the option will always get validated, independently of register_setting()
187 * (only available on back-end).
188 */
189 add_filter( 'sanitize_option_' . $this->option_name, [ $this, 'validate' ] );
190
191 // Flushes the rewrite rules when option is updated.
192 add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_rewrites' ] );
193
194 /* Register our option for the admin pages */
195 add_action( 'admin_init', [ $this, 'register_setting' ] );
196
197
198 /* Set option group name if not given */
199 if ( ! isset( $this->group_name ) || $this->group_name === '' ) {
200 $this->group_name = 'yoast_' . $this->option_name . '_options';
201 }
202
203 /* Translate some defaults as early as possible - textdomain is loaded in init on priority 1. */
204 if ( method_exists( $this, 'translate_defaults' ) ) {
205 add_action( 'init', [ $this, 'translate_defaults' ], 2 );
206 }
207
208 /**
209 * Enrich defaults once custom post types and taxonomies have been registered
210 * which is normally done on the init action.
211 *
212 * @todo [JRF/testers] Verify that none of the options which are only available after
213 * enrichment are used before the enriching.
214 */
215 if ( method_exists( $this, 'enrich_defaults' ) ) {
216 add_action( 'init', [ $this, 'enrich_defaults' ], 99 );
217 }
218 }
219
220 /*
221 * All concrete classes *must* contain the get_instance method.
222 *
223 * {@internal Unfortunately I can't define it as an abstract as it also *has* to be static...}}
224 *
225 * ```
226 * abstract protected static function get_instance();
227 * ```
228 * ---------------
229 *
230 * Concrete classes *may* contain a translate_defaults method.
231 * ```
232 * abstract public function translate_defaults();
233 * ```
234 * ---------------
235 *
236 * Concrete classes *may* contain a enrich_defaults method to add additional defaults once
237 * all post_types and taxonomies have been registered.
238 *
239 * ```
240 * abstract public function enrich_defaults();
241 * ```
242 */
243
244 /* *********** METHODS INFLUENCING get_option() *********** */
245
246 /**
247 * Add filters to make sure that the option default is returned if the option is not set.
248 *
249 * @return void
250 */
251 public function add_default_filters() {
252 // Don't change, needs to check for false as could return prio 0 which would evaluate to false.
253 if ( has_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] ) === false ) {
254 add_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] );
255 }
256 }
257
258 /**
259 * Adds back the default filters that were removed during validation if the option was changed.
260 * Checks if this option was changed to prevent constantly checking if filters are present.
261 *
262 * @param string $option_name The option name.
263 *
264 * @return void
265 */
266 public function add_default_filters_if_same_option( $option_name ) {
267 if ( $option_name === $this->option_name ) {
268 $this->add_default_filters();
269 }
270 }
271
272 /**
273 * Adds back the default filters that were removed during validation if the option was not changed.
274 * This is because in that case the latter actions are not called and thus the filters are never
275 * added back.
276 *
277 * @param mixed $value The current value.
278 * @param string $option_name The option name.
279 * @param mixed $old_value The old value.
280 *
281 * @return string The current value.
282 */
283 public function add_default_filters_if_not_changed( $value, $option_name, $old_value ) {
284 if ( $option_name !== $this->option_name ) {
285 return $value;
286 }
287
288 if ( $value === $old_value || maybe_serialize( $value ) === maybe_serialize( $old_value ) ) {
289 $this->add_default_filters();
290 }
291
292 return $value;
293 }
294
295 /**
296 * Validate webmaster tools & Pinterest verification strings.
297 *
298 * @param string $key Key to check, by type of service.
299 * @param array $dirty Dirty data with the new values.
300 * @param array $old Old data.
301 * @param array $clean Clean data by reference, normally the default values.
302 */
303 public function validate_verification_string( $key, $dirty, $old, &$clean ) {
304 if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
305 $meta = $dirty[ $key ];
306 if ( strpos( $meta, 'content=' ) ) {
307 // Make sure we only have the real key, not a complete meta tag.
308 preg_match( '`content=([\'"])?([^\'"> ]+)(?:\1|[ />])`', $meta, $match );
309 if ( isset( $match[2] ) ) {
310 $meta = $match[2];
311 }
312 unset( $match );
313 }
314
315 $meta = sanitize_text_field( $meta );
316 if ( $meta !== '' ) {
317 $regex = '`^[A-Fa-f0-9_-]+$`';
318 $service = '';
319
320 switch ( $key ) {
321 case 'baiduverify':
322 $regex = '`^[A-Za-z0-9_-]+$`';
323 $service = 'Baidu Webmaster tools';
324 break;
325
326 case 'googleverify':
327 $regex = '`^[A-Za-z0-9_-]+$`';
328 $service = 'Google Webmaster tools';
329 break;
330
331 case 'msverify':
332 $service = 'Bing Webmaster tools';
333 break;
334
335 case 'pinterestverify':
336 $service = 'Pinterest';
337 break;
338
339 case 'yandexverify':
340 $service = 'Yandex Webmaster tools';
341 break;
342 }
343
344 if ( preg_match( $regex, $meta ) ) {
345 $clean[ $key ] = $meta;
346 }
347 else {
348 // Restore the previous value, if any.
349 if ( isset( $old[ $key ] ) && preg_match( $regex, $old[ $key ] ) ) {
350 $clean[ $key ] = $old[ $key ];
351 }
352
353 if ( function_exists( 'add_settings_error' ) ) {
354 add_settings_error(
355 $this->group_name, // Slug title of the setting.
356 $key, // Suffix-ID for the error message box. WordPress prepends `setting-error-`.
357 /* translators: 1: Verification string from user input; 2: Service name. */
358 sprintf( __( '%1$s does not seem to be a valid %2$s verification string. Please correct.', 'wordpress-seo' ), '<strong>' . esc_html( $meta ) . '</strong>', $service ), // The error message.
359 'error' // CSS class for the WP notice, either the legacy 'error' / 'updated' or the new `notice-*` ones.
360 );
361 }
362
363 Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $meta );
364 }
365 }
366 }
367 }
368
369 /**
370 * Validates an option as a valid URL. Prints out a WordPress settings error
371 * notice if the URL is invalid.
372 *
373 * @param string $key Key to check, by type of URL setting.
374 * @param array $dirty Dirty data with the new values.
375 * @param array $old Old data.
376 * @param array $clean Clean data by reference, normally the default values.
377 */
378 public function validate_url( $key, $dirty, $old, &$clean ) {
379 if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
380
381 $submitted_url = trim( htmlspecialchars( $dirty[ $key ], ENT_COMPAT, get_bloginfo( 'charset' ), true ) );
382 $validated_url = filter_var( WPSEO_Utils::sanitize_url( $submitted_url ), FILTER_VALIDATE_URL );
383
384 if ( $validated_url === false ) {
385 if ( function_exists( 'add_settings_error' ) ) {
386 add_settings_error(
387 // Slug title of the setting.
388 $this->group_name,
389 // Suffix-ID for the error message box. WordPress prepends `setting-error-`.
390 $key,
391 // The error message.
392 sprintf(
393 /* translators: %s expands to an invalid URL. */
394 __( '%s does not seem to be a valid url. Please correct.', 'wordpress-seo' ),
395 '<strong>' . esc_html( $submitted_url ) . '</strong>'
396 ),
397 // Message type.
398 'error'
399 );
400 }
401
402 // Restore the previous URL value, if any.
403 if ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) {
404 $url = WPSEO_Utils::sanitize_url( $old[ $key ] );
405 if ( $url !== '' ) {
406 $clean[ $key ] = $url;
407 }
408 }
409
410 Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $submitted_url );
411
412 return;
413 }
414
415 // The URL format is valid, let's sanitize it.
416 $url = WPSEO_Utils::sanitize_url( $validated_url );
417
418 if ( $url !== '' ) {
419 $clean[ $key ] = $url;
420 }
421 }
422 }
423
424 /**
425 * Validates a Facebook App ID.
426 *
427 * @deprecated 15.5
428 * @codeCoverageIgnore
429 *
430 * @param string $key Key to check, in this case: the Facebook App ID field name.
431 * @param array $dirty Dirty data with the new values.
432 * @param array $old Old data.
433 * @param array $clean Clean data by reference, normally the default values.
434 */
435 public function validate_facebook_app_id( $key, $dirty, $old, &$clean ) {
436 _deprecated_function( __METHOD__, 'WPSEO 15.5' );
437
438 if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
439 $url = 'https://graph.facebook.com/' . $dirty[ $key ];
440
441 $response = wp_remote_get( $url );
442
443 /**
444 * Filter: 'validate_facebook_app_id_api_response_code' - Allows to filter the Faceboook API response code.
445 *
446 * @deprecated 15.5
447 *
448 * @api int $response_code The Facebook API response header code.
449 */
450 $response_code = apply_filters_deprecated( 'validate_facebook_app_id_api_response_code', wp_remote_retrieve_response_code( $response ), 'WPSEO 15.5' );
451
452 /**
453 * Filter: 'validate_facebook_app_id_api_response_body' - Allows to filter the Faceboook API response body.
454 *
455 * @deprecated 15.5
456 *
457 * @api string $response_body The Facebook API JSON response body.
458 */
459 $response_body = apply_filters_deprecated( 'validate_facebook_app_id_api_response_body', wp_remote_retrieve_body( $response ), 'WPSEO 15.5' );
460 $response_object = json_decode( $response_body );
461
462 /*
463 * When the request is successful the response code will be 200 and
464 * the response object will contain an `id` property.
465 */
466 if ( $response_code === 200 && isset( $response_object->id ) ) {
467 $clean[ $key ] = $dirty[ $key ];
468 return;
469 }
470
471 // Restore the previous value, if any.
472 if ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) {
473 $clean[ $key ] = $old[ $key ];
474 }
475
476 if ( function_exists( 'add_settings_error' ) ) {
477 add_settings_error(
478 $this->group_name, // Slug title of the setting.
479 $key, // Suffix-ID for the error message box. WordPress prepends `setting-error-`.
480 sprintf(
481 /* translators: %s expands to an invalid Facebook App ID. */
482 __( '%s does not seem to be a valid Facebook App ID. Please correct.', 'wordpress-seo' ),
483 '<strong>' . esc_html( $dirty[ $key ] ) . '</strong>'
484 ), // The error message.
485 'error' // CSS class for the WP notice, either the legacy 'error' / 'updated' or the new `notice-*` ones.
486 );
487 }
488
489 Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $dirty[ $key ] );
490 }
491 }
492
493 /**
494 * Remove the default filters.
495 * Called from the validate() method to prevent failure to add new options.
496 *
497 * @return void
498 */
499 public function remove_default_filters() {
500 remove_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] );
501 }
502
503 /**
504 * Get the enriched default value for an option.
505 *
506 * Checks if the concrete class contains an enrich_defaults() method and if so, runs it.
507 *
508 * {@internal The enrich_defaults method is used to set defaults for variable array keys
509 * in an option, such as array keys depending on post_types and/or taxonomies.}}
510 *
511 * @return array
512 */
513 public function get_defaults() {
514 if ( method_exists( $this, 'translate_defaults' ) ) {
515 $this->translate_defaults();
516 }
517
518 if ( method_exists( $this, 'enrich_defaults' ) ) {
519 $this->enrich_defaults();
520 }
521
522 return apply_filters( 'wpseo_defaults', $this->defaults, $this->option_name );
523 }
524
525 /**
526 * Add filters to make sure that the option is merged with its defaults before being returned.
527 *
528 * @return void
529 */
530 public function add_option_filters() {
531 // Don't change, needs to check for false as could return prio 0 which would evaluate to false.
532 if ( has_filter( 'option_' . $this->option_name, [ $this, 'get_option' ] ) === false ) {
533 add_filter( 'option_' . $this->option_name, [ $this, 'get_option' ] );
534 }
535 }
536
537 /**
538 * Remove the option filters.
539 * Called from the clean_up methods to make sure we retrieve the original old option.
540 *
541 * @return void
542 */
543 public function remove_option_filters() {
544 remove_filter( 'option_' . $this->option_name, [ $this, 'get_option' ] );
545 }
546
547 /**
548 * Merge an option with its default values.
549 *
550 * This method should *not* be called directly!!! It is only meant to filter the get_option() results.
551 *
552 * @param mixed $options Option value.
553 *
554 * @return mixed Option merged with the defaults for that option.
555 */
556 public function get_option( $options = null ) {
557 $filtered = $this->array_filter_merge( $options );
558
559 /*
560 * If the option contains variable option keys, make sure we don't remove those settings
561 * - even if the defaults are not complete yet.
562 * Unfortunately this means we also won't be removing the settings for post types or taxonomies
563 * which are no longer in the WP install, but rather that than the other way around.
564 */
565 if ( isset( $this->variable_array_key_patterns ) ) {
566 $filtered = $this->retain_variable_keys( $options, $filtered );
567 }
568
569 return $filtered;
570 }
571
572 /* *********** METHODS influencing add_uption(), update_option() and saving from admin pages. *********** */
573
574 /**
575 * Register (whitelist) the option for the configuration pages.
576 * The validation callback is already registered separately on the sanitize_option hook,
577 * so no need to double register.
578 *
579 * @return void
580 */
581 public function register_setting() {
582 if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) {
583 return;
584 }
585
586 if ( $this->multisite_only === true ) {
587 $network_settings_api = Yoast_Network_Settings_API::get();
588 if ( $network_settings_api->meets_requirements() ) {
589 $network_settings_api->register_setting( $this->group_name, $this->option_name );
590 }
591 return;
592 }
593
594 register_setting( $this->group_name, $this->option_name );
595 }
596
597 /**
598 * Validate the option
599 *
600 * @param mixed $option_value The unvalidated new value for the option.
601 *
602 * @return array Validated new value for the option.
603 */
604 public function validate( $option_value ) {
605 $clean = $this->get_defaults();
606
607 /* Return the defaults if the new value is empty. */
608 if ( ! is_array( $option_value ) || $option_value === [] ) {
609 return $clean;
610 }
611
612 $option_value = array_map( [ 'WPSEO_Utils', 'trim_recursive' ], $option_value );
613
614 $old = $this->get_original_option();
615 if ( ! is_array( $old ) ) {
616 $old = [];
617 }
618 $old = array_merge( $clean, $old );
619
620 $clean = $this->validate_option( $option_value, $clean, $old );
621
622 // Prevent updates to variables that are disabled via the override option.
623 $clean = $this->prevent_disabled_options_update( $clean, $old );
624
625 /* Retain the values for variable array keys even when the post type/taxonomy is not yet registered. */
626 if ( isset( $this->variable_array_key_patterns ) ) {
627 $clean = $this->retain_variable_keys( $option_value, $clean );
628 }
629
630 $this->remove_default_filters();
631
632 return $clean;
633 }
634
635 /**
636 * Checks whether a specific option key is disabled.
637 *
638 * This is determined by whether an override option is available with a key that equals the given key prefixed
639 * with 'allow_'.
640 *
641 * @param string $key Option key.
642 *
643 * @return bool True if option key is disabled, false otherwise.
644 */
645 public function is_disabled( $key ) {
646 $override_option = $this->get_override_option();
647 if ( empty( $override_option ) ) {
648 return false;
649 }
650
651 return isset( $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) && ! $override_option[ self::ALLOW_KEY_PREFIX . $key ];
652 }
653
654 /**
655 * All concrete classes must contain a validate_option() method which validates all
656 * values within the option.
657 *
658 * @param array $dirty New value for the option.
659 * @param array $clean Clean value for the option, normally the defaults.
660 * @param array $old Old value of the option.
661 */
662 abstract protected function validate_option( $dirty, $clean, $old );
663
664 /* *********** METHODS for ADDING/UPDATING/UPGRADING the option. *********** */
665
666 /**
667 * Retrieve the real old value (unmerged with defaults).
668 *
669 * @return array|bool The original option value (which can be false if the option doesn't exist).
670 */
671 protected function get_original_option() {
672 $this->remove_default_filters();
673 $this->remove_option_filters();
674
675 // Get (unvalidated) array, NOT merged with defaults.
676 if ( $this->multisite_only !== true ) {
677 $option_value = get_option( $this->option_name );
678 }
679 else {
680 $option_value = get_site_option( $this->option_name );
681 }
682
683 $this->add_option_filters();
684 $this->add_default_filters();
685
686 return $option_value;
687 }
688
689 /**
690 * Add the option if it doesn't exist for some strange reason.
691 *
692 * @uses WPSEO_Option::get_original_option()
693 *
694 * @return void
695 */
696 public function maybe_add_option() {
697 if ( $this->get_original_option() === false ) {
698 if ( $this->multisite_only !== true ) {
699 update_option( $this->option_name, $this->get_defaults() );
700 }
701 else {
702 $this->update_site_option( $this->get_defaults() );
703 }
704 }
705 }
706
707 /**
708 * Update a site_option.
709 *
710 * {@internal This special method is only needed for multisite options, but very needed indeed there.
711 * The order in which certain functions and hooks are run is different between
712 * get_option() and get_site_option() which means in practice that the removing
713 * of the default filters would be done too late and the re-adding of the default
714 * filters might not be done at all.
715 * Aka: use the WPSEO_Options::update_site_option() method (which calls this method)
716 * for safely adding/updating multisite options.}}
717 *
718 * @param mixed $value The new value for the option.
719 *
720 * @return bool Whether the update was succesfull.
721 */
722 public function update_site_option( $value ) {
723 if ( $this->multisite_only === true && is_multisite() ) {
724 $this->remove_default_filters();
725 $result = update_site_option( $this->option_name, $value );
726 $this->add_default_filters();
727
728 return $result;
729 }
730 else {
731 return false;
732 }
733 }
734
735 /**
736 * Retrieve the real old value (unmerged with defaults), clean and re-save the option.
737 *
738 * @uses WPSEO_Option::get_original_option()
739 * @uses WPSEO_Option::import()
740 *
741 * @param string|null $current_version Optional. Version from which to upgrade, if not set,
742 * version specific upgrades will be disregarded.
743 *
744 * @return void
745 */
746 public function clean( $current_version = null ) {
747 $option_value = $this->get_original_option();
748 $this->import( $option_value, $current_version );
749 }
750
751 /**
752 * Clean and re-save the option.
753 *
754 * @uses clean_option() method from concrete class if it exists.
755 *
756 * @todo [JRF/whomever] Figure out a way to show settings error during/after the upgrade - maybe
757 * something along the lines of:
758 * -> add them to a property in this class
759 * -> if that property isset at the end of the routine and add_settings_error function does not exist,
760 * save as transient (or update the transient if one already exists)
761 * -> next time an admin is in the WP back-end, show the errors and delete the transient or only delete it
762 * once the admin has dismissed the message (add ajax function)
763 * Important: all validation routines which add_settings_errors would need to be changed for this to work
764 *
765 * @param array $option_value Option value to be imported.
766 * @param string|null $current_version Optional. Version from which to upgrade, if not set,
767 * version specific upgrades will be disregarded.
768 * @param array|null $all_old_option_values Optional. Only used when importing old options to
769 * have access to the real old values, in contrast to
770 * the saved ones.
771 *
772 * @return void
773 */
774 public function import( $option_value, $current_version = null, $all_old_option_values = null ) {
775 if ( $option_value === false ) {
776 $option_value = $this->get_defaults();
777 }
778 elseif ( is_array( $option_value ) && method_exists( $this, 'clean_option' ) ) {
779 $option_value = $this->clean_option( $option_value, $current_version, $all_old_option_values );
780 }
781
782 /*
783 * Save the cleaned value - validation will take care of cleaning out array keys which
784 * should no longer be there.
785 */
786 if ( $this->multisite_only !== true ) {
787 update_option( $this->option_name, $option_value );
788 }
789 else {
790 $this->update_site_option( $this->option_name, $option_value );
791 }
792 }
793
794 /**
795 * Returns the variable array key patterns for an options class.
796 *
797 * @return array
798 */
799 public function get_patterns() {
800 return (array) $this->variable_array_key_patterns;
801 }
802
803 /**
804 * Retrieves the option name.
805 *
806 * @return string The set option name.
807 */
808 public function get_option_name() {
809 return $this->option_name;
810 }
811
812 /*
813 * Concrete classes *may* contain a clean_option method which will clean out old/renamed
814 * values within the option.
815 *
816 * ```
817 * abstract public function clean_option( $option_value, $current_version = null, $all_old_option_values = null );
818 * ```
819 */
820
821 /* *********** HELPER METHODS for internal use. *********** */
822
823 /**
824 * Helper method - Combines a fixed array of default values with an options array
825 * while filtering out any keys which are not in the defaults array.
826 *
827 * @todo [JRF] - shouldn't this be a straight array merge ? at the end of the day, the validation
828 * removes any invalid keys on save.
829 *
830 * @param array|null $options Optional. Current options. If not set, the option defaults
831 * for the $option_key will be returned.
832 *
833 * @return array Combined and filtered options array.
834 */
835 protected function array_filter_merge( $options = null ) {
836
837 $defaults = $this->get_defaults();
838
839 if ( ! isset( $options ) || $options === false || $options === [] ) {
840 return $defaults;
841 }
842
843 $options = (array) $options;
844
845 /*
846 $filtered = array();
847
848 if ( $defaults !== array() ) {
849 foreach ( $defaults as $key => $default_value ) {
850 // @todo should this walk through array subkeys ?
851 $filtered[ $key ] = ( isset( $options[ $key ] ) ? $options[ $key ] : $default_value );
852 }
853 }
854 */
855 $filtered = array_merge( $defaults, $options );
856
857 return $filtered;
858 }
859
860 /**
861 * Sets updated values for variables that are disabled via the override option back to their previous values.
862 *
863 * @param array $updated Updated option value.
864 * @param array $old Old option value.
865 *
866 * @return array Updated option value, with all disabled variables set to their old values.
867 */
868 protected function prevent_disabled_options_update( $updated, $old ) {
869 $override_option = $this->get_override_option();
870 if ( empty( $override_option ) ) {
871 return $updated;
872 }
873
874 /*
875 * This loop could as well call `is_disabled( $key )` for each iteration,
876 * however this would be worse performance-wise.
877 */
878 foreach ( $old as $key => $value ) {
879 if ( isset( $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) && ! $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) {
880 $updated[ $key ] = $old[ $key ];
881 }
882 }
883
884 return $updated;
885 }
886
887 /**
888 * Retrieves the value of the override option, if available.
889 *
890 * An override option contains values that may determine access to certain sub-variables
891 * of this option.
892 *
893 * Only regular options in multisite can have override options, which in that case
894 * would be network options.
895 *
896 * @return array Override option value, or empty array if unavailable.
897 */
898 protected function get_override_option() {
899 if ( empty( $this->override_option_name ) || $this->multisite_only === true || ! is_multisite() ) {
900 return [];
901 }
902
903 return get_site_option( $this->override_option_name, [] );
904 }
905
906 /**
907 * Make sure that any set option values relating to post_types and/or taxonomies are retained,
908 * even when that post_type or taxonomy may not yet have been registered.
909 *
910 * {@internal The wpseo_titles concrete class overrules this method. Make sure that any
911 * changes applied here, also get ported to that version.}}
912 *
913 * @param array $dirty Original option as retrieved from the database.
914 * @param array $clean Filtered option where any options which shouldn't be in our option
915 * have already been removed and any options which weren't set
916 * have been set to their defaults.
917 *
918 * @return array
919 */
920 protected function retain_variable_keys( $dirty, $clean ) {
921 if ( ( is_array( $this->variable_array_key_patterns ) && $this->variable_array_key_patterns !== [] ) && ( is_array( $dirty ) && $dirty !== [] ) ) {
922 foreach ( $dirty as $key => $value ) {
923
924 // Do nothing if already in filtered options.
925 if ( isset( $clean[ $key ] ) ) {
926 continue;
927 }
928
929 foreach ( $this->variable_array_key_patterns as $pattern ) {
930
931 if ( strpos( $key, $pattern ) === 0 ) {
932 $clean[ $key ] = $value;
933 break;
934 }
935 }
936 }
937 }
938
939 return $clean;
940 }
941
942 /**
943 * Check whether a given array key conforms to one of the variable array key patterns for this option.
944 *
945 * @usedby validate_option() methods for options with variable array keys.
946 *
947 * @param string $key Array key to check.
948 *
949 * @return string Pattern if it conforms, original array key if it doesn't or if the option
950 * does not have variable array keys.
951 */
952 protected function get_switch_key( $key ) {
953 if ( ! isset( $this->variable_array_key_patterns ) || ( ! is_array( $this->variable_array_key_patterns ) || $this->variable_array_key_patterns === [] ) ) {
954 return $key;
955 }
956
957 foreach ( $this->variable_array_key_patterns as $pattern ) {
958 if ( strpos( $key, $pattern ) === 0 ) {
959 return $pattern;
960 }
961 }
962
963 return $key;
964 }
965 }
966