PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.0
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.0
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-wpseo.php

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

702 lines 24.9 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 * Option: wpseo.
10 */
11 class WPSEO_Option_Wpseo extends WPSEO_Option {
12
13 /**
14 * Option name.
15 *
16 * @var string
17 */
18 public $option_name = 'wpseo';
19
20 /**
21 * Array of defaults for the option.
22 *
23 * {@internal Shouldn't be requested directly, use $this->get_defaults();}}
24 *
25 * @var array
26 */
27 protected $defaults = [
28 // Non-form fields, set via (ajax) function.
29 'tracking' => null,
30 'toggled_tracking' => false,
31 'license_server_version' => false,
32 'ms_defaults_set' => false,
33 'ignore_search_engines_discouraged_notice' => false,
34 'indexing_first_time' => true,
35 'indexing_started' => null,
36 'indexing_reason' => '',
37 'indexables_indexing_completed' => false,
38 'index_now_key' => '',
39 // Non-form field, should only be set via validation routine.
40 'version' => '', // Leave default as empty to ensure activation/upgrade works.
41 'previous_version' => '',
42 // Form fields.
43 'disableadvanced_meta' => true,
44 'enable_headless_rest_endpoints' => true,
45 'ryte_indexability' => false,
46 'baiduverify' => '', // Text field.
47 'googleverify' => '', // Text field.
48 'msverify' => '', // Text field.
49 'yandexverify' => '',
50 'ahrefsverify' => '',
51 'site_type' => '', // List of options.
52 'has_multiple_authors' => '',
53 'environment_type' => '',
54 'content_analysis_active' => true,
55 'keyword_analysis_active' => true,
56 'inclusive_language_analysis_active' => false,
57 'enable_admin_bar_menu' => true,
58 'enable_cornerstone_content' => true,
59 'enable_xml_sitemap' => true,
60 'enable_text_link_counter' => true,
61 'enable_index_now' => true,
62 'enable_ai_generator' => true,
63 'ai_enabled_pre_default' => false,
64 'show_onboarding_notice' => false,
65 'first_activated_on' => false,
66 'myyoast-oauth' => [
67 'config' => [
68 'clientId' => null,
69 'secret' => null,
70 ],
71 'access_tokens' => [],
72 ],
73 'semrush_integration_active' => true,
74 'semrush_tokens' => [],
75 'semrush_country_code' => 'us',
76 'permalink_structure' => '',
77 'home_url' => '',
78 'dynamic_permalinks' => false,
79 'category_base_url' => '',
80 'tag_base_url' => '',
81 'custom_taxonomy_slugs' => [],
82 'enable_enhanced_slack_sharing' => true,
83 'enable_metabox_insights' => true,
84 'enable_link_suggestions' => true,
85 'algolia_integration_active' => false,
86 'import_cursors' => [],
87 'workouts_data' => [ 'configuration' => [ 'finishedSteps' => [] ] ],
88 'configuration_finished_steps' => [],
89 'dismiss_configuration_workout_notice' => false,
90 'dismiss_premium_deactivated_notice' => false,
91 'importing_completed' => [],
92 'wincher_integration_active' => true,
93 'wincher_tokens' => [],
94 'wincher_automatically_add_keyphrases' => false,
95 'wincher_website_id' => '',
96 'first_time_install' => false,
97 'should_redirect_after_install_free' => false,
98 'activation_redirect_timestamp_free' => 0,
99 'remove_feed_global' => false,
100 'remove_feed_global_comments' => false,
101 'remove_feed_post_comments' => false,
102 'remove_feed_authors' => false,
103 'remove_feed_categories' => false,
104 'remove_feed_tags' => false,
105 'remove_feed_custom_taxonomies' => false,
106 'remove_feed_post_types' => false,
107 'remove_feed_search' => false,
108 'remove_atom_rdf_feeds' => false,
109 'remove_shortlinks' => false,
110 'remove_rest_api_links' => false,
111 'remove_rsd_wlw_links' => false,
112 'remove_oembed_links' => false,
113 'remove_generator' => false,
114 'remove_emoji_scripts' => false,
115 'remove_powered_by_header' => false,
116 'remove_pingback_header' => false,
117 'clean_campaign_tracking_urls' => false,
118 'clean_permalinks' => false,
119 'clean_permalinks_extra_variables' => '',
120 'search_cleanup' => false,
121 'search_cleanup_emoji' => false,
122 'search_cleanup_patterns' => false,
123 'search_character_limit' => 50,
124 'deny_search_crawling' => false,
125 'deny_wp_json_crawling' => false,
126 'deny_adsbot_crawling' => false,
127 'deny_ccbot_crawling' => false,
128 'deny_google_extended_crawling' => false,
129 'deny_gptbot_crawling' => false,
130 'redirect_search_pretty_urls' => false,
131 'least_readability_ignore_list' => [],
132 'least_seo_score_ignore_list' => [],
133 'most_linked_ignore_list' => [],
134 'least_linked_ignore_list' => [],
135 'indexables_page_reading_list' => [ false, false, false, false, false ],
136 'indexables_overview_state' => 'dashboard-not-visited',
137 'last_known_public_post_types' => [],
138 'last_known_public_taxonomies' => [],
139 'last_known_no_unindexed' => [],
140 'new_post_types' => [],
141 'new_taxonomies' => [],
142 'show_new_content_type_notification' => false,
143 'site_kit_configuration_permanently_dismissed' => false,
144 'site_kit_connected' => false,
145 'site_kit_tracking_setup_widget_loaded' => 'no',
146 'site_kit_tracking_first_interaction_stage' => '',
147 'site_kit_tracking_last_interaction_stage' => '',
148 'site_kit_tracking_setup_widget_temporarily_dismissed' => 'no',
149 'site_kit_tracking_setup_widget_permanently_dismissed' => 'no',
150 'google_site_kit_feature_enabled' => false, // No longer used.
151 'ai_free_sparks_started_on' => null,
152 'enable_llms_txt' => false,
153 'last_updated_on' => false,
154 'default_seo_title' => [],
155 'default_seo_meta_desc' => [],
156 'first_activated_by' => 0,
157 'enable_schema_aggregation_endpoint' => false,
158 'schema_aggregation_endpoint_enabled_on' => null,
159 'enable_task_list' => true,
160 'enable_schema' => true,
161 ];
162
163 /**
164 * Sub-options which should not be overloaded with multi-site defaults.
165 *
166 * @var array
167 */
168 public $ms_exclude = [
169 'ignore_search_engines_discouraged_notice',
170 /* Privacy. */
171 'baiduverify',
172 'googleverify',
173 'msverify',
174 'yandexverify',
175 'ahrefsverify',
176 ];
177
178 /**
179 * Possible values for the site_type option.
180 *
181 * @var array
182 */
183 protected $site_types = [
184 '',
185 'blog',
186 'shop',
187 'news',
188 'smallBusiness',
189 'corporateOther',
190 'personalOther',
191 ];
192
193 /**
194 * Possible environment types.
195 *
196 * @var array
197 */
198 protected $environment_types = [
199 '',
200 'local',
201 'production',
202 'staging',
203 'development',
204 ];
205
206 /**
207 * Possible has_multiple_authors options.
208 *
209 * @var array
210 */
211 protected $has_multiple_authors_options = [
212 '',
213 true,
214 false,
215 ];
216
217 /**
218 * Name for an option higher in the hierarchy to override setting access.
219 *
220 * @var string
221 */
222 protected $override_option_name = 'wpseo_ms';
223
224 /**
225 * Add the actions and filters for the option.
226 *
227 * @todo [JRF => testers] Check if the extra actions below would run into problems if an option
228 * is updated early on and if so, change the call to schedule these for a later action on add/update
229 * instead of running them straight away.
230 */
231 protected function __construct() {
232 parent::__construct();
233
234 /**
235 * Filter: 'wpseo_enable_tracking' - Enables the data tracking of Yoast SEO Premium.
236 *
237 * @param string|false $is_enabled The enabled state. Default is false.
238 */
239 $this->defaults['tracking'] = apply_filters( 'wpseo_enable_tracking', false );
240
241 /* Clear the cache on update/add. */
242 add_action( 'add_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_cache' ] );
243 add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_cache' ] );
244
245 add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] );
246
247 /**
248 * Filter the `wpseo` option defaults.
249 *
250 * @param array $defaults Array the defaults for the `wpseo` option attributes.
251 */
252 $this->defaults = apply_filters( 'wpseo_option_wpseo_defaults', $this->defaults );
253 }
254
255 /**
256 * Get the singleton instance of this class.
257 *
258 * @return object
259 */
260 public static function get_instance() {
261 if ( ! ( self::$instance instanceof self ) ) {
262 self::$instance = new self();
263 }
264
265 return self::$instance;
266 }
267
268 /**
269 * Add filters to make sure that the option is merged with its defaults before being returned.
270 *
271 * @return void
272 */
273 public function add_option_filters() {
274 parent::add_option_filters();
275
276 list( $hookname, $callback, $priority ) = $this->get_verify_features_option_filter_hook();
277
278 if ( has_filter( $hookname, $callback ) === false ) {
279 add_filter( $hookname, $callback, $priority );
280 }
281 }
282
283 /**
284 * Remove the option filters.
285 * Called from the clean_up methods to make sure we retrieve the original old option.
286 *
287 * @return void
288 */
289 public function remove_option_filters() {
290 parent::remove_option_filters();
291
292 list( $hookname, $callback, $priority ) = $this->get_verify_features_option_filter_hook();
293
294 remove_filter( $hookname, $callback, $priority );
295 }
296
297 /**
298 * Add filters to make sure that the option default is returned if the option is not set.
299 *
300 * @return void
301 */
302 public function add_default_filters() {
303 parent::add_default_filters();
304
305 list( $hookname, $callback, $priority ) = $this->get_verify_features_default_option_filter_hook();
306
307 if ( has_filter( $hookname, $callback ) === false ) {
308 add_filter( $hookname, $callback, $priority );
309 }
310 }
311
312 /**
313 * Remove the default filters.
314 * Called from the validate() method to prevent failure to add new options.
315 *
316 * @return void
317 */
318 public function remove_default_filters() {
319 parent::remove_default_filters();
320
321 list( $hookname, $callback, $priority ) = $this->get_verify_features_default_option_filter_hook();
322
323 remove_filter( $hookname, $callback, $priority );
324 }
325
326 /**
327 * Validate the option.
328 *
329 * @param array $dirty New value for the option.
330 * @param array $clean Clean value for the option, normally the defaults.
331 * @param array $old Old value of the option.
332 *
333 * @return array Validated clean value for the option to be saved to the database.
334 */
335 protected function validate_option( $dirty, $clean, $old ) {
336
337 foreach ( $clean as $key => $value ) {
338 switch ( $key ) {
339 case 'version':
340 $clean[ $key ] = WPSEO_VERSION;
341 break;
342 case 'previous_version':
343 case 'semrush_country_code':
344 case 'license_server_version':
345 case 'home_url':
346 case 'index_now_key':
347 case 'wincher_website_id':
348 case 'clean_permalinks_extra_variables':
349 case 'indexables_overview_state':
350 if ( isset( $dirty[ $key ] ) ) {
351 $clean[ $key ] = $dirty[ $key ];
352 }
353 break;
354 case 'indexing_reason':
355 case 'site_kit_tracking_setup_widget_loaded':
356 case 'site_kit_tracking_first_interaction_stage':
357 case 'site_kit_tracking_last_interaction_stage':
358 case 'site_kit_tracking_setup_widget_temporarily_dismissed':
359 case 'site_kit_tracking_setup_widget_permanently_dismissed':
360 case 'ai_free_sparks_started_on':
361 case 'schema_aggregation_endpoint_enabled_on':
362 if ( isset( $dirty[ $key ] ) ) {
363 $clean[ $key ] = sanitize_text_field( $dirty[ $key ] );
364 }
365 break;
366
367 /* Verification strings. */
368 case 'baiduverify':
369 case 'googleverify':
370 case 'msverify':
371 case 'yandexverify':
372 case 'ahrefsverify':
373 $this->validate_verification_string( $key, $dirty, $old, $clean );
374 break;
375
376 /*
377 * Boolean dismiss warnings - not fields - may not be in form
378 * (and don't need to be either as long as the default is false).
379 */
380 case 'ignore_search_engines_discouraged_notice':
381 case 'ms_defaults_set':
382 if ( isset( $dirty[ $key ] ) ) {
383 $clean[ $key ] = WPSEO_Utils::validate_bool( $dirty[ $key ] );
384 }
385 elseif ( isset( $old[ $key ] ) ) {
386 $clean[ $key ] = WPSEO_Utils::validate_bool( $old[ $key ] );
387 }
388 break;
389
390 case 'site_type':
391 $clean[ $key ] = $old[ $key ];
392 if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], $this->site_types, true ) ) {
393 $clean[ $key ] = $dirty[ $key ];
394 }
395 break;
396
397 case 'environment_type':
398 $clean[ $key ] = $old[ $key ];
399 if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], $this->environment_types, true ) ) {
400 $clean[ $key ] = $dirty[ $key ];
401 }
402 break;
403
404 case 'has_multiple_authors':
405 $clean[ $key ] = $old[ $key ];
406 if ( isset( $dirty[ $key ] ) && in_array( $dirty[ $key ], $this->has_multiple_authors_options, true ) ) {
407 $clean[ $key ] = $dirty[ $key ];
408 }
409
410 break;
411
412 case 'first_activated_on':
413 case 'indexing_started':
414 case 'activation_redirect_timestamp_free':
415 case 'last_updated_on':
416 $clean[ $key ] = false;
417 if ( isset( $dirty[ $key ] ) ) {
418 if ( $dirty[ $key ] === false || WPSEO_Utils::validate_int( $dirty[ $key ] ) ) {
419 $clean[ $key ] = $dirty[ $key ];
420 }
421 }
422 break;
423
424 case 'first_activated_by':
425 // A slight change from the other integer fields, as we want to allow '0' here, but don't want to have much impact elsewhere.
426 $clean[ $key ] = false;
427 if ( isset( $dirty[ $key ] ) ) {
428 if ( $dirty[ $key ] === false || WPSEO_Utils::validate_int( $dirty[ $key ] ) !== false ) {
429 $clean[ $key ] = $dirty[ $key ];
430 }
431 }
432 break;
433
434 case 'tracking':
435 $clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : null );
436 break;
437
438 case 'myyoast_oauth':
439 case 'semrush_tokens':
440 case 'custom_taxonomy_slugs':
441 case 'wincher_tokens':
442 case 'workouts_data':
443 case 'configuration_finished_steps':
444 case 'least_readability_ignore_list':
445 case 'least_seo_score_ignore_list':
446 case 'most_linked_ignore_list':
447 case 'least_linked_ignore_list':
448 case 'indexables_page_reading_list':
449 case 'last_known_public_post_types':
450 case 'last_known_public_taxonomies':
451 case 'new_post_types':
452 case 'new_taxonomies':
453 case 'default_seo_title':
454 case 'default_seo_meta_desc':
455 $clean[ $key ] = $old[ $key ];
456
457 if ( isset( $dirty[ $key ] ) ) {
458 $items = $dirty[ $key ];
459 if ( ! is_array( $items ) ) {
460 $items = json_decode( $dirty[ $key ], true );
461 }
462
463 if ( is_array( $items ) ) {
464 $clean[ $key ] = $dirty[ $key ];
465 }
466 }
467
468 break;
469
470 case 'permalink_structure':
471 case 'category_base_url':
472 case 'tag_base_url':
473 if ( isset( $dirty[ $key ] ) ) {
474 $clean[ $key ] = sanitize_option( $key, $dirty[ $key ] );
475 }
476 break;
477
478 case 'search_character_limit':
479 if ( isset( $dirty[ $key ] ) ) {
480 $clean[ $key ] = (int) $dirty[ $key ];
481 }
482 break;
483
484 case 'import_cursors':
485 case 'importing_completed':
486 if ( isset( $dirty[ $key ] ) && is_array( $dirty[ $key ] ) ) {
487 $clean[ $key ] = $dirty[ $key ];
488 }
489 break;
490
491 case 'last_known_no_unindexed':
492 $clean[ $key ] = $old[ $key ];
493
494 if ( isset( $dirty[ $key ] ) ) {
495 $items = $dirty[ $key ];
496
497 if ( is_array( $items ) ) {
498 foreach ( $items as $item_key => $item ) {
499 if ( ! is_string( $item_key ) || ! is_numeric( $item ) ) {
500 unset( $items[ $item_key ] );
501 }
502 }
503 $clean[ $key ] = $items;
504 }
505 }
506
507 break;
508
509 /*
510 * Boolean (checkbox) fields.
511 *
512 * Covers:
513 * 'disableadvanced_meta'
514 * 'enable_headless_rest_endpoints'
515 * 'yoast_tracking'
516 * 'dynamic_permalinks'
517 * 'indexing_first_time'
518 * 'first_time_install'
519 * 'remove_feed_global'
520 * 'remove_feed_global_comments'
521 * 'remove_feed_post_comments'
522 * 'remove_feed_authors'
523 * 'remove_feed_categories'
524 * 'remove_feed_tags'
525 * 'remove_feed_custom_taxonomies'
526 * 'remove_feed_post_types'
527 * 'remove_feed_search'
528 * 'remove_atom_rdf_feeds'
529 * 'remove_shortlinks'
530 * 'remove_rest_api_links'
531 * 'remove_rsd_wlw_links'
532 * 'remove_oembed_links'
533 * 'remove_generator'
534 * 'remove_emoji_scripts'
535 * 'remove_powered_by_header'
536 * 'remove_pingback_header'
537 * 'clean_campaign_tracking_urls'
538 * 'clean_permalinks'
539 * 'clean_permalinks_extra_variables'
540 * 'search_cleanup'
541 * 'search_cleanup_emoji'
542 * 'search_cleanup_patterns'
543 * 'deny_wp_json_crawling'
544 * 'deny_adsbot_crawling'
545 * 'deny_ccbot_crawling'
546 * 'deny_google_extended_crawling'
547 * 'deny_gptbot_crawling'
548 * 'redirect_search_pretty_urls'
549 * 'should_redirect_after_install_free'
550 * 'show_new_content_type_notification'
551 * 'site_kit_configuration_permanently_dismissed',
552 * 'site_kit_connected',
553 * 'google_site_kit_feature_enabled',
554 * 'enable_llms_txt',
555 * 'enable_schema_aggregation_endpoint'
556 * 'enable_task_list',
557 * 'enable_schema',
558 * and most of the feature variables.
559 */
560 default:
561 $clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false );
562 break;
563 }
564 }
565
566 return $clean;
567 }
568
569 /**
570 * Verifies that the feature variables are turned off if the network is configured so.
571 *
572 * @param mixed $options Value of the option to be returned. Typically an array.
573 *
574 * @return mixed Filtered $options value.
575 */
576 public function verify_features_against_network( $options = [] ) {
577 if ( ! is_array( $options ) || empty( $options ) ) {
578 return $options;
579 }
580
581 // For the feature variables, set their values to off in case they are disabled.
582 $feature_vars = [
583 'disableadvanced_meta' => false,
584 'ryte_indexability' => false,
585 'content_analysis_active' => false,
586 'keyword_analysis_active' => false,
587 'inclusive_language_analysis_active' => false,
588 'enable_admin_bar_menu' => false,
589 'enable_cornerstone_content' => false,
590 'enable_xml_sitemap' => false,
591 'enable_text_link_counter' => false,
592 'enable_metabox_insights' => false,
593 'enable_link_suggestions' => false,
594 'enable_headless_rest_endpoints' => false,
595 'tracking' => false,
596 'enable_enhanced_slack_sharing' => false,
597 'semrush_integration_active' => false,
598 'wincher_integration_active' => false,
599 'remove_feed_global' => false,
600 'remove_feed_global_comments' => false,
601 'remove_feed_post_comments' => false,
602 'enable_index_now' => false,
603 'enable_ai_generator' => false,
604 'remove_feed_authors' => false,
605 'remove_feed_categories' => false,
606 'remove_feed_tags' => false,
607 'remove_feed_custom_taxonomies' => false,
608 'remove_feed_post_types' => false,
609 'remove_feed_search' => false,
610 'remove_atom_rdf_feeds' => false,
611 'remove_shortlinks' => false,
612 'remove_rest_api_links' => false,
613 'remove_rsd_wlw_links' => false,
614 'remove_oembed_links' => false,
615 'remove_generator' => false,
616 'remove_emoji_scripts' => false,
617 'remove_powered_by_header' => false,
618 'remove_pingback_header' => false,
619 'clean_campaign_tracking_urls' => false,
620 'clean_permalinks' => false,
621 'search_cleanup' => false,
622 'search_cleanup_emoji' => false,
623 'search_cleanup_patterns' => false,
624 'redirect_search_pretty_urls' => false,
625 'algolia_integration_active' => false,
626 'google_site_kit_feature_enabled' => false,
627 'enable_llms_txt' => false,
628 'enable_task_list' => false,
629 'enable_schema_aggregation_endpoint' => false,
630 'enable_schema' => false,
631 ];
632
633 // We can reuse this logic from the base class with the above defaults to parse with the correct feature values.
634 $options = $this->prevent_disabled_options_update( $options, $feature_vars );
635
636 return $options;
637 }
638
639 /**
640 * Gets the filter hook name and callback for adjusting the retrieved option value
641 * against the network-allowed features.
642 *
643 * @return array Array where the first item is the hook name, the second is the hook callback,
644 * and the third is the hook priority.
645 */
646 protected function get_verify_features_option_filter_hook() {
647 return [
648 "option_{$this->option_name}",
649 [ $this, 'verify_features_against_network' ],
650 11,
651 ];
652 }
653
654 /**
655 * Gets the filter hook name and callback for adjusting the default option value against the network-allowed features.
656 *
657 * @return array Array where the first item is the hook name, the second is the hook callback,
658 * and the third is the hook priority.
659 */
660 protected function get_verify_features_default_option_filter_hook() {
661 return [
662 "default_option_{$this->option_name}",
663 [ $this, 'verify_features_against_network' ],
664 11,
665 ];
666 }
667
668 /**
669 * Clean a given option value.
670 *
671 * @param array $option_value Old (not merged with defaults or filtered) option value to
672 * clean according to the rules for this option.
673 * @param string|null $current_version Optional. Version from which to upgrade, if not set,
674 * version specific upgrades will be disregarded.
675 * @param array|null $all_old_option_values Optional. Only used when importing old options to have
676 * access to the real old values, in contrast to the saved ones.
677 *
678 * @return array Cleaned option.
679 */
680 protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) {
681 // Deal with value change from text string to boolean.
682 $value_change = [
683 'ignore_search_engines_discouraged_notice',
684 ];
685
686 $target_values = [
687 'ignore',
688 'done',
689 ];
690
691 foreach ( $value_change as $key ) {
692 if ( isset( $option_value[ $key ] )
693 && in_array( $option_value[ $key ], $target_values, true )
694 ) {
695 $option_value[ $key ] = true;
696 }
697 }
698
699 return $option_value;
700 }
701 }
702