• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / dd6e866a9e6d253114633104d9e3858d807178ba

19 Jun 2024 10:03AM UTC coverage: 48.628% (-4.3%) from 52.936%
dd6e866a9e6d253114633104d9e3858d807178ba

push

github

web-flow
Merge pull request #21431 from Yoast/21429-update-copy-in-the-introduction-and-consent-modals

Updates the copy for the introduction and consent modals

7441 of 13454 branches covered (55.31%)

Branch coverage included in aggregate %.

0 of 3 new or added lines in 2 files covered. (0.0%)

3718 existing lines in 107 files now uncovered.

25100 of 53464 relevant lines covered (46.95%)

62392.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/inc/options/class-wpseo-option.php
1
<?php
2
/**
3
 * WPSEO plugin file.
4
 *
5
 * @package WPSEO\Internals\Options
6
 */
7

8
/**
9
 * This abstract class and its 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 as the classes here
26
 *   are instantiated, validation for all options and their subkeys will be automatic.
27
 * - On (successful) 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 existence.
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
        public 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 conforms to the normal pattern 'yoast' . $option_name . 'options',
78
         * only set in concrete 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
                if ( $this->multisite_only !== true ) {
×
153
                        /**
154
                         * The option validation routines remove the default filters to prevent failing
155
                         * to insert an option if it's new. Let's add them back afterwards.
156
                         */
157
                        add_action( 'add_option', [ $this, 'add_default_filters_if_same_option' ] ); // Adding back after INSERT.
×
158

159
                        add_action( 'update_option', [ $this, 'add_default_filters_if_same_option' ] );
×
160

161
                        add_filter( 'pre_update_option', [ $this, 'add_default_filters_if_not_changed' ], PHP_INT_MAX, 3 );
×
162

163
                        // Refills the cache when the option has been updated.
164
                        add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Options', 'clear_cache' ], 10 );
×
165
                }
166
                elseif ( is_multisite() ) {
×
167
                        /*
168
                         * The option validation routines remove the default filters to prevent failing
169
                         * to insert an option if it's new. Let's add them back afterwards.
170
                         *
171
                         * For site_options, this method is not foolproof as these actions are not fired
172
                         * on an insert/update failure. Please use the WPSEO_Options::update_site_option() method
173
                         * for updating site options to make sure the filters are in place.
174
                         */
175
                        add_action( 'add_site_option_' . $this->option_name, [ $this, 'add_default_filters' ] );
×
176
                        add_action( 'update_site_option_' . $this->option_name, [ $this, 'add_default_filters' ] );
×
177
                        add_filter( 'pre_update_site_option_' . $this->option_name, [ $this, 'add_default_filters_if_not_changed' ], PHP_INT_MAX, 3 );
×
178

179
                        // Refills the cache when the option has been updated.
180
                        add_action( 'update_site_option_' . $this->option_name, [ 'WPSEO_Options', 'clear_cache' ], 1, 0 );
×
181
                }
182

183
                /*
184
                 * Make sure the option will always get validated, independently of register_setting()
185
                 * (only available on back-end).
186
                 */
187
                add_filter( 'sanitize_option_' . $this->option_name, [ $this, 'validate' ] );
×
188

189
                /* Register our option for the admin pages */
190
                add_action( 'admin_init', [ $this, 'register_setting' ] );
×
191

192
                /* Set option group name if not given */
193
                if ( ! isset( $this->group_name ) || $this->group_name === '' ) {
×
194
                        $this->group_name = 'yoast_' . $this->option_name . '_options';
×
195
                }
196

197
                /* Translate some defaults as early as possible - textdomain is loaded in init on priority 1. */
198
                if ( method_exists( $this, 'translate_defaults' ) ) {
×
199
                        add_action( 'init', [ $this, 'translate_defaults' ], 2 );
×
200
                }
201

202
                /**
203
                 * Enrich defaults once custom post types and taxonomies have been registered
204
                 * which is normally done on the init action.
205
                 *
206
                 * @todo [JRF/testers] Verify that none of the options which are only available after
207
                 * enrichment are used before the enriching.
208
                 */
209
                if ( method_exists( $this, 'enrich_defaults' ) ) {
×
210
                        add_action( 'init', [ $this, 'enrich_defaults' ], 99 );
×
211
                }
212
        }
213

214
        /*
215
         * All concrete classes *must* contain the get_instance method.
216
         *
217
         * {@internal Unfortunately I can't define it as an abstract as it also *has* to be static...}}
218
         *
219
         * ```
220
         * abstract protected static function get_instance();
221
         * ```
222
         * ---------------
223
         *
224
         * Concrete classes *may* contain a translate_defaults method.
225
         * ```
226
         * abstract public function translate_defaults();
227
         * ```
228
         * ---------------
229
         *
230
         * Concrete classes *may* contain an enrich_defaults method to add additional defaults once
231
         * all post_types and taxonomies have been registered.
232
         *
233
         * ```
234
         * abstract public function enrich_defaults();
235
         * ```
236
         */
237

238
        /* *********** METHODS INFLUENCING get_option() *********** */
239

240
        /**
241
         * Add filters to make sure that the option default is returned if the option is not set.
242
         *
243
         * @return void
244
         */
245
        public function add_default_filters() {
×
246
                // Don't change, needs to check for false as could return prio 0 which would evaluate to false.
247
                if ( has_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] ) === false ) {
×
248
                        add_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] );
×
249
                }
250
        }
251

252
        /**
253
         * Adds back the default filters that were removed during validation if the option was changed.
254
         * Checks if this option was changed to prevent constantly checking if filters are present.
255
         *
256
         * @param string $option_name The option name.
257
         *
258
         * @return void
259
         */
260
        public function add_default_filters_if_same_option( $option_name ) {
×
261
                if ( $option_name === $this->option_name ) {
×
262
                        $this->add_default_filters();
×
263
                }
264
        }
265

266
        /**
267
         * Adds back the default filters that were removed during validation if the option was not changed.
268
         * This is because in that case the latter actions are not called and thus the filters are never
269
         * added back.
270
         *
271
         * @param mixed  $value       The current value.
272
         * @param string $option_name The option name.
273
         * @param mixed  $old_value   The old value.
274
         *
275
         * @return string The current value.
276
         */
277
        public function add_default_filters_if_not_changed( $value, $option_name, $old_value ) {
×
278
                if ( $option_name !== $this->option_name ) {
×
279
                        return $value;
×
280
                }
281

282
                if ( $value === $old_value || maybe_serialize( $value ) === maybe_serialize( $old_value ) ) {
×
283
                        $this->add_default_filters();
×
284
                }
285

286
                return $value;
×
287
        }
288

289
        /**
290
         * Validate webmaster tools & Pinterest verification strings.
291
         *
292
         * @param string $key   Key to check, by type of service.
293
         * @param array  $dirty Dirty data with the new values.
294
         * @param array  $old   Old data.
295
         * @param array  $clean Clean data by reference, normally the default values.
296
         *
297
         * @return void
298
         */
299
        public function validate_verification_string( $key, $dirty, $old, &$clean ) {
×
300
                if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
×
301
                        $meta = $dirty[ $key ];
×
302
                        if ( strpos( $meta, 'content=' ) ) {
×
303
                                // Make sure we only have the real key, not a complete meta tag.
304
                                preg_match( '`content=([\'"])?([^\'"> ]+)(?:\1|[ />])`', $meta, $match );
×
305
                                if ( isset( $match[2] ) ) {
×
306
                                        $meta = $match[2];
×
307
                                }
308
                                unset( $match );
×
309
                        }
310

311
                        $meta = sanitize_text_field( $meta );
×
312
                        if ( $meta !== '' ) {
×
313
                                $regex   = '`^[A-Fa-f0-9_-]+$`';
×
314
                                $service = '';
×
315

316
                                switch ( $key ) {
317
                                        case 'baiduverify':
×
318
                                                $regex   = '`^[A-Za-z0-9_-]+$`';
×
319
                                                $service = 'Baidu Webmaster tools';
×
320
                                                break;
×
321

322
                                        case 'googleverify':
×
323
                                                $regex   = '`^[A-Za-z0-9_-]+$`';
×
324
                                                $service = 'Google Webmaster tools';
×
325
                                                break;
×
326

327
                                        case 'msverify':
×
328
                                                $service = 'Bing Webmaster tools';
×
329
                                                break;
×
330

331
                                        case 'pinterestverify':
×
332
                                                $service = 'Pinterest';
×
333
                                                break;
×
334

335
                                        case 'yandexverify':
×
336
                                                $service = 'Yandex Webmaster tools';
×
337
                                                break;
×
338
                                }
339

340
                                if ( preg_match( $regex, $meta ) ) {
×
341
                                        $clean[ $key ] = $meta;
×
342
                                }
343
                                else {
344
                                        // Restore the previous value, if any.
345
                                        if ( isset( $old[ $key ] ) && preg_match( $regex, $old[ $key ] ) ) {
×
346
                                                $clean[ $key ] = $old[ $key ];
×
347
                                        }
348

349
                                        if ( function_exists( 'add_settings_error' ) ) {
×
350
                                                add_settings_error(
×
351
                                                        $this->group_name, // Slug title of the setting.
×
352
                                                        $key, // Suffix-ID for the error message box. WordPress prepends `setting-error-`.
×
353
                                                        /* translators: 1: Verification string from user input; 2: Service name. */
354
                                                        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.
×
355
                                                        'error' // CSS class for the WP notice, either the legacy 'error' / 'updated' or the new `notice-*` ones.
×
356
                                                );
357
                                        }
358

359
                                        Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $meta );
×
360
                                }
361
                        }
362
                }
363
        }
364

365
        /**
366
         * Validates an option as a valid URL. Prints out a WordPress settings error
367
         * notice if the URL is invalid.
368
         *
369
         * @param string $key   Key to check, by type of URL setting.
370
         * @param array  $dirty Dirty data with the new values.
371
         * @param array  $old   Old data.
372
         * @param array  $clean Clean data by reference, normally the default values.
373
         *
374
         * @return void
375
         */
376
        public function validate_url( $key, $dirty, $old, &$clean ) {
×
377
                if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
×
378

379
                        $submitted_url = trim( $dirty[ $key ] );
×
380
                        $validated_url = filter_var( WPSEO_Utils::sanitize_url( $submitted_url ), FILTER_VALIDATE_URL );
×
381

382
                        if ( $validated_url === false ) {
×
383
                                if ( function_exists( 'add_settings_error' ) ) {
×
384
                                        add_settings_error(
×
385
                                                // Slug title of the setting.
386
                                                $this->group_name,
×
387
                                                // Suffix-ID for the error message box. WordPress prepends `setting-error-`.
388
                                                $key,
×
389
                                                // The error message.
390
                                                sprintf(
×
391
                                                        /* translators: %s expands to an invalid URL. */
392
                                                        __( '%s does not seem to be a valid url. Please correct.', 'wordpress-seo' ),
×
393
                                                        '<strong>' . esc_url( $submitted_url ) . '</strong>'
×
394
                                                ),
395
                                                // Message type.
396
                                                'error'
×
397
                                        );
398
                                }
399

400
                                // Restore the previous URL value, if any.
401
                                if ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) {
×
402
                                        $url = WPSEO_Utils::sanitize_url( $old[ $key ] );
×
403
                                        if ( $url !== '' ) {
×
404
                                                $clean[ $key ] = $url;
×
405
                                        }
406
                                }
407

408
                                Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $submitted_url );
×
409

410
                                return;
×
411
                        }
412

413
                        // The URL format is valid, let's sanitize it.
414
                        $url = WPSEO_Utils::sanitize_url( $validated_url );
×
415

416
                        if ( $url !== '' ) {
×
417
                                $clean[ $key ] = $url;
×
418
                        }
419
                }
420
        }
421

422
        /**
423
         * Remove the default filters.
424
         * Called from the validate() method to prevent failure to add new options.
425
         *
426
         * @return void
427
         */
428
        public function remove_default_filters() {
×
429
                remove_filter( 'default_option_' . $this->option_name, [ $this, 'get_defaults' ] );
×
430
        }
431

432
        /**
433
         * Get the enriched default value for an option.
434
         *
435
         * Checks if the concrete class contains an enrich_defaults() method and if so, runs it.
436
         *
437
         * {@internal The enrich_defaults method is used to set defaults for variable array keys
438
         *            in an option, such as array keys depending on post_types and/or taxonomies.}}
439
         *
440
         * @return array
441
         */
442
        public function get_defaults() {
×
443
                if ( method_exists( $this, 'translate_defaults' ) ) {
×
444
                        $this->translate_defaults();
×
445
                }
446

447
                if ( method_exists( $this, 'enrich_defaults' ) ) {
×
448
                        $this->enrich_defaults();
×
449
                }
450

451
                return apply_filters( 'wpseo_defaults', $this->defaults, $this->option_name );
×
452
        }
453

454
        /**
455
         * Add filters to make sure that the option is merged with its defaults before being returned.
456
         *
457
         * @return void
458
         */
459
        public function add_option_filters() {
×
460
                // Don't change, needs to check for false as could return prio 0 which would evaluate to false.
461
                if ( has_filter( 'option_' . $this->option_name, [ $this, 'get_option' ] ) === false ) {
×
462
                        add_filter( 'option_' . $this->option_name, [ $this, 'get_option' ] );
×
463
                }
464
        }
465

466
        /**
467
         * Remove the option filters.
468
         * Called from the clean_up methods to make sure we retrieve the original old option.
469
         *
470
         * @return void
471
         */
472
        public function remove_option_filters() {
×
473
                remove_filter( 'option_' . $this->option_name, [ $this, 'get_option' ] );
×
474
        }
475

476
        /**
477
         * Merge an option with its default values.
478
         *
479
         * This method should *not* be called directly!!! It is only meant to filter the get_option() results.
480
         *
481
         * @param mixed $options Option value.
482
         *
483
         * @return mixed Option merged with the defaults for that option.
484
         */
UNCOV
485
        public function get_option( $options = null ) {
×
UNCOV
486
                $filtered = $this->array_filter_merge( $options );
×
487

488
                /*
489
                 * If the option contains variable option keys, make sure we don't remove those settings
490
                 * - even if the defaults are not complete yet.
491
                 * Unfortunately this means we also won't be removing the settings for post types or taxonomies
492
                 * which are no longer in the WP install, but rather that than the other way around.
493
                 */
UNCOV
494
                if ( isset( $this->variable_array_key_patterns ) ) {
×
495
                        $filtered = $this->retain_variable_keys( $options, $filtered );
×
496
                }
497

UNCOV
498
                return $filtered;
×
499
        }
500

501
        /* *********** METHODS influencing add_option(), update_option() and saving from admin pages. *********** */
502

503
        /**
504
         * Register (whitelist) the option for the configuration pages.
505
         * The validation callback is already registered separately on the sanitize_option hook,
506
         * so no need to double register.
507
         *
508
         * @return void
509
         */
510
        public function register_setting() {
×
511
                if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) {
×
512
                        return;
×
513
                }
514

515
                if ( $this->multisite_only === true ) {
×
516
                        $network_settings_api = Yoast_Network_Settings_API::get();
×
517
                        if ( $network_settings_api->meets_requirements() ) {
×
518
                                $network_settings_api->register_setting( $this->group_name, $this->option_name );
×
519
                        }
520
                        return;
×
521
                }
522

523
                register_setting( $this->group_name, $this->option_name );
×
524
        }
525

526
        /**
527
         * Validate the option.
528
         *
529
         * @param mixed $option_value The unvalidated new value for the option.
530
         *
531
         * @return array Validated new value for the option.
532
         */
UNCOV
533
        public function validate( $option_value ) {
×
UNCOV
534
                $clean = $this->get_defaults();
×
535

536
                /* Return the defaults if the new value is empty. */
UNCOV
537
                if ( ! is_array( $option_value ) || $option_value === [] ) {
×
538
                        return $clean;
×
539
                }
540

UNCOV
541
                $option_value = array_map( [ 'WPSEO_Utils', 'trim_recursive' ], $option_value );
×
542

UNCOV
543
                $old = $this->get_original_option();
×
UNCOV
544
                if ( ! is_array( $old ) ) {
×
545
                        $old = [];
×
546
                }
UNCOV
547
                $old = array_merge( $clean, $old );
×
548

UNCOV
549
                $clean = $this->validate_option( $option_value, $clean, $old );
×
550

551
                // Prevent updates to variables that are disabled via the override option.
UNCOV
552
                $clean = $this->prevent_disabled_options_update( $clean, $old );
×
553

554
                /* Retain the values for variable array keys even when the post type/taxonomy is not yet registered. */
UNCOV
555
                if ( isset( $this->variable_array_key_patterns ) ) {
×
556
                        $clean = $this->retain_variable_keys( $option_value, $clean );
×
557
                }
558

UNCOV
559
                $this->remove_default_filters();
×
560

UNCOV
561
                return $clean;
×
562
        }
563

564
        /**
565
         * Checks whether a specific option key is disabled.
566
         *
567
         * This is determined by whether an override option is available with a key that equals the given key prefixed
568
         * with 'allow_'.
569
         *
570
         * @param string $key Option key.
571
         *
572
         * @return bool True if option key is disabled, false otherwise.
573
         */
574
        public function is_disabled( $key ) {
×
575
                $override_option = $this->get_override_option();
×
576
                if ( empty( $override_option ) ) {
×
577
                        return false;
×
578
                }
579

580
                return isset( $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) && ! $override_option[ self::ALLOW_KEY_PREFIX . $key ];
×
581
        }
582

583
        /**
584
         * All concrete classes must contain a validate_option() method which validates all
585
         * values within the option.
586
         *
587
         * @param array $dirty New value for the option.
588
         * @param array $clean Clean value for the option, normally the defaults.
589
         * @param array $old   Old value of the option.
590
         */
591
        abstract protected function validate_option( $dirty, $clean, $old );
592

593
        /* *********** METHODS for ADDING/UPDATING/UPGRADING the option. *********** */
594

595
        /**
596
         * Retrieve the real old value (unmerged with defaults).
597
         *
598
         * @return array|bool The original option value (which can be false if the option doesn't exist).
599
         */
600
        protected function get_original_option() {
×
601
                $this->remove_default_filters();
×
602
                $this->remove_option_filters();
×
603

604
                // Get (unvalidated) array, NOT merged with defaults.
605
                if ( $this->multisite_only !== true ) {
×
606
                        $option_value = get_option( $this->option_name );
×
607
                }
608
                else {
609
                        $option_value = get_site_option( $this->option_name );
×
610
                }
611

612
                $this->add_option_filters();
×
613
                $this->add_default_filters();
×
614

615
                return $option_value;
×
616
        }
617

618
        /**
619
         * Add the option if it doesn't exist for some strange reason.
620
         *
621
         * @uses WPSEO_Option::get_original_option()
622
         *
623
         * @return void
624
         */
625
        public function maybe_add_option() {
×
626
                if ( $this->get_original_option() === false ) {
×
627
                        if ( $this->multisite_only !== true ) {
×
628
                                update_option( $this->option_name, $this->get_defaults() );
×
629
                        }
630
                        else {
631
                                $this->update_site_option( $this->get_defaults() );
×
632
                        }
633
                }
634
        }
635

636
        /**
637
         * Update a site_option.
638
         *
639
         * {@internal This special method is only needed for multisite options, but very needed indeed there.
640
         *            The order in which certain functions and hooks are run is different between
641
         *            get_option() and get_site_option() which means in practice that the removing
642
         *            of the default filters would be done too late and the re-adding of the default
643
         *            filters might not be done at all.
644
         *            Aka: use the WPSEO_Options::update_site_option() method (which calls this method)
645
         *            for safely adding/updating multisite options.}}
646
         *
647
         * @param mixed $value The new value for the option.
648
         *
649
         * @return bool Whether the update was successful.
650
         */
651
        public function update_site_option( $value ) {
×
652
                if ( $this->multisite_only === true && is_multisite() ) {
×
653
                        $this->remove_default_filters();
×
654
                        $result = update_site_option( $this->option_name, $value );
×
655
                        $this->add_default_filters();
×
656

657
                        return $result;
×
658
                }
659
                else {
660
                        return false;
×
661
                }
662
        }
663

664
        /**
665
         * Retrieve the real old value (unmerged with defaults), clean and re-save the option.
666
         *
667
         * @uses WPSEO_Option::get_original_option()
668
         * @uses WPSEO_Option::import()
669
         *
670
         * @param string|null $current_version Optional. Version from which to upgrade, if not set,
671
         *                                     version-specific upgrades will be disregarded.
672
         *
673
         * @return void
674
         */
675
        public function clean( $current_version = null ) {
×
676
                $option_value = $this->get_original_option();
×
677
                $this->import( $option_value, $current_version );
×
678
        }
679

680
        /**
681
         * Clean and re-save the option.
682
         *
683
         * @uses clean_option() method from concrete class if it exists.
684
         *
685
         * @todo [JRF/whomever] Figure out a way to show settings error during/after the upgrade - maybe
686
         * something along the lines of:
687
         * -> add them to a property in this class
688
         * -> if that property isset at the end of the routine and add_settings_error function does not exist,
689
         *    save as transient (or update the transient if one already exists)
690
         * -> next time an admin is in the WP back-end, show the errors and delete the transient or only delete it
691
         *    once the admin has dismissed the message (add ajax function)
692
         * Important: all validation routines which add_settings_errors would need to be changed for this to work
693
         *
694
         * @param array       $option_value          Option value to be imported.
695
         * @param string|null $current_version       Optional. Version from which to upgrade, if not set,
696
         *                                           version-specific upgrades will be disregarded.
697
         * @param array|null  $all_old_option_values Optional. Only used when importing old options to
698
         *                                           have access to the real old values, in contrast to
699
         *                                           the saved ones.
700
         *
701
         * @return void
702
         */
703
        public function import( $option_value, $current_version = null, $all_old_option_values = null ) {
×
704
                if ( $option_value === false ) {
×
705
                        $option_value = $this->get_defaults();
×
706
                }
707
                elseif ( is_array( $option_value ) && method_exists( $this, 'clean_option' ) ) {
×
708
                        $option_value = $this->clean_option( $option_value, $current_version, $all_old_option_values );
×
709
                }
710

711
                /*
712
                 * Save the cleaned value - validation will take care of cleaning out array keys which
713
                 * should no longer be there.
714
                 */
715
                if ( $this->multisite_only !== true ) {
×
716
                        update_option( $this->option_name, $option_value );
×
717
                }
718
                else {
719
                        $this->update_site_option( $this->option_name, $option_value );
×
720
                }
721
        }
722

723
        /**
724
         * Returns the variable array key patterns for an options class.
725
         *
726
         * @return array
727
         */
728
        public function get_patterns() {
×
729
                return (array) $this->variable_array_key_patterns;
×
730
        }
731

732
        /**
733
         * Retrieves the option name.
734
         *
735
         * @return string The set option name.
736
         */
737
        public function get_option_name() {
×
738
                return $this->option_name;
×
739
        }
740

741
        /*
742
         * Concrete classes *may* contain a clean_option method which will clean out old/renamed
743
         * values within the option.
744
         *
745
         * ```
746
         * abstract public function clean_option( $option_value, $current_version = null, $all_old_option_values = null );
747
         * ```
748
         */
749

750
        /* *********** HELPER METHODS for internal use. *********** */
751

752
        /**
753
         * Helper method - Combines a fixed array of default values with an options array
754
         * while filtering out any keys which are not in the defaults array.
755
         *
756
         * @todo [JRF] - shouldn't this be a straight array merge ? at the end of the day, the validation
757
         * removes any invalid keys on save.
758
         *
759
         * @param array|null $options Optional. Current options. If not set, the option defaults
760
         *                            for the $option_key will be returned.
761
         *
762
         * @return array Combined and filtered options array.
763
         */
764
        protected function array_filter_merge( $options = null ) {
×
765

766
                $defaults = $this->get_defaults();
×
767

768
                if ( ! isset( $options ) || $options === false || $options === [] ) {
×
769
                        return $defaults;
×
770
                }
771

772
                $options = (array) $options;
×
773

774
                /*
775
                        $filtered = array();
776

777
                        if ( $defaults !== array() ) {
778
                                foreach ( $defaults as $key => $default_value ) {
779
                                        // @todo should this walk through array subkeys ?
780
                                        $filtered[ $key ] = ( isset( $options[ $key ] ) ? $options[ $key ] : $default_value );
781
                                }
782
                        }
783
                */
784
                $filtered = array_merge( $defaults, $options );
×
785

786
                return $filtered;
×
787
        }
788

789
        /**
790
         * Sets updated values for variables that are disabled via the override option back to their previous values.
791
         *
792
         * @param array $updated Updated option value.
793
         * @param array $old     Old option value.
794
         *
795
         * @return array Updated option value, with all disabled variables set to their old values.
796
         */
UNCOV
797
        protected function prevent_disabled_options_update( $updated, $old ) {
×
UNCOV
798
                $override_option = $this->get_override_option();
×
UNCOV
799
                if ( empty( $override_option ) ) {
×
UNCOV
800
                        return $updated;
×
801
                }
802

803
                /*
804
                 * This loop could as well call `is_disabled( $key )` for each iteration,
805
                 * however this would be worse performance-wise.
806
                 */
UNCOV
807
                foreach ( $old as $key => $value ) {
×
UNCOV
808
                        if ( isset( $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) && ! $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) {
×
UNCOV
809
                                $updated[ $key ] = $old[ $key ];
×
810
                        }
811
                }
812

UNCOV
813
                return $updated;
×
814
        }
815

816
        /**
817
         * Retrieves the value of the override option, if available.
818
         *
819
         * An override option contains values that may determine access to certain sub-variables
820
         * of this option.
821
         *
822
         * Only regular options in multisite can have override options, which in that case
823
         * would be network options.
824
         *
825
         * @return array Override option value, or empty array if unavailable.
826
         */
827
        protected function get_override_option() {
×
828
                if ( empty( $this->override_option_name ) || $this->multisite_only === true || ! is_multisite() ) {
×
829
                        return [];
×
830
                }
831

832
                return get_site_option( $this->override_option_name, [] );
×
833
        }
834

835
        /**
836
         * Make sure that any set option values relating to post_types and/or taxonomies are retained,
837
         * even when that post_type or taxonomy may not yet have been registered.
838
         *
839
         * {@internal The wpseo_titles concrete class overrules this method. Make sure that any
840
         *            changes applied here, also get ported to that version.}}
841
         *
842
         * @param array $dirty Original option as retrieved from the database.
843
         * @param array $clean Filtered option where any options which shouldn't be in our option
844
         *                     have already been removed and any options which weren't set
845
         *                     have been set to their defaults.
846
         *
847
         * @return array
848
         */
849
        protected function retain_variable_keys( $dirty, $clean ) {
×
850
                if ( ( is_array( $this->variable_array_key_patterns ) && $this->variable_array_key_patterns !== [] ) && ( is_array( $dirty ) && $dirty !== [] ) ) {
×
851
                        foreach ( $dirty as $key => $value ) {
×
852

853
                                // Do nothing if already in filtered options.
854
                                if ( isset( $clean[ $key ] ) ) {
×
855
                                        continue;
×
856
                                }
857

858
                                foreach ( $this->variable_array_key_patterns as $pattern ) {
×
859

860
                                        if ( strpos( $key, $pattern ) === 0 ) {
×
861
                                                $clean[ $key ] = $value;
×
862
                                                break;
×
863
                                        }
864
                                }
865
                        }
866
                }
867

868
                return $clean;
×
869
        }
870

871
        /**
872
         * Check whether a given array key conforms to one of the variable array key patterns for this option.
873
         *
874
         * @used-by validate_option() methods for options with variable array keys.
875
         *
876
         * @param string $key Array key to check.
877
         *
878
         * @return string Pattern if it conforms, original array key if it doesn't or if the option
879
         *                does not have variable array keys.
880
         */
881
        protected function get_switch_key( $key ) {
×
882
                if ( ! isset( $this->variable_array_key_patterns ) || ( ! is_array( $this->variable_array_key_patterns ) || $this->variable_array_key_patterns === [] ) ) {
×
883
                        return $key;
×
884
                }
885

886
                foreach ( $this->variable_array_key_patterns as $pattern ) {
×
887
                        if ( strpos( $key, $pattern ) === 0 ) {
×
888
                                return $pattern;
×
889
                        }
890
                }
891

892
                return $key;
×
893
        }
894
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc