PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
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-titles.php

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

1,006 lines 34.3 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 use Yoast\WP\SEO\Config\Schema_Types;
9
10 /**
11 * Option: wpseo_titles.
12 */
13 class WPSEO_Option_Titles extends WPSEO_Option {
14
15 /**
16 * Option name.
17 *
18 * @var string
19 */
20 public $option_name = 'wpseo_titles';
21
22 /**
23 * Array of defaults for the option.
24 *
25 * Shouldn't be requested directly, use $this->get_defaults();
26 *
27 * {@internal Note: Some of the default values are added via the translate_defaults() method.}}
28 *
29 * @var string[]
30 */
31 protected $defaults = [
32 // Form fields.
33 'forcerewritetitle' => false,
34 'separator' => 'sc-dash',
35 'title-home-wpseo' => '%%sitename%% %%page%% %%sep%% %%sitedesc%%', // Text field.
36 'title-author-wpseo' => '', // Text field.
37 'title-archive-wpseo' => '%%date%% %%page%% %%sep%% %%sitename%%', // Text field.
38 'title-search-wpseo' => '', // Text field.
39 'title-404-wpseo' => '', // Text field.
40
41 'social-title-author-wpseo' => '%%name%%', // Text field.
42 'social-title-archive-wpseo' => '%%date%%', // Text field.
43 'social-description-author-wpseo' => '', // Text area.
44 'social-description-archive-wpseo' => '', // Text area.
45 'social-image-url-author-wpseo' => '', // Hidden input field.
46 'social-image-url-archive-wpseo' => '', // Hidden input field.
47 'social-image-id-author-wpseo' => 0, // Hidden input field.
48 'social-image-id-archive-wpseo' => 0, // Hidden input field.
49
50 'metadesc-home-wpseo' => '', // Text area.
51 'metadesc-author-wpseo' => '', // Text area.
52 'metadesc-archive-wpseo' => '', // Text area.
53 'rssbefore' => '', // Text area.
54 'rssafter' => '', // Text area.
55
56 'noindex-author-wpseo' => false,
57 'noindex-author-noposts-wpseo' => true,
58 'noindex-archive-wpseo' => true,
59
60 'disable-author' => false,
61 'disable-date' => false,
62 'disable-post_format' => false,
63 'disable-attachment' => true,
64
65 'breadcrumbs-404crumb' => '', // Text field.
66 'breadcrumbs-display-blog-page' => true,
67 'breadcrumbs-boldlast' => false,
68 'breadcrumbs-archiveprefix' => '', // Text field.
69 'breadcrumbs-enable' => true,
70 'breadcrumbs-home' => '', // Text field.
71 'breadcrumbs-prefix' => '', // Text field.
72 'breadcrumbs-searchprefix' => '', // Text field.
73 'breadcrumbs-sep' => '»', // Text field.
74
75 'website_name' => '',
76 'person_name' => '',
77 'person_logo' => '',
78 'person_logo_id' => 0,
79 'alternate_website_name' => '',
80 'company_logo' => '',
81 'company_logo_id' => 0,
82 'company_logo_meta' => false,
83 'person_logo_meta' => false,
84 'company_name' => '',
85 'company_alternate_name' => '',
86 'company_or_person' => 'company',
87 'company_or_person_user_id' => false,
88
89 'stripcategorybase' => false,
90
91 'open_graph_frontpage_title' => '%%sitename%%', // Text field.
92 'open_graph_frontpage_desc' => '', // Text field.
93 'open_graph_frontpage_image' => '', // Text field.
94 'open_graph_frontpage_image_id' => 0,
95
96 'publishing_principles_id' => 0,
97 'ownership_funding_info_id' => 0,
98 'actionable_feedback_policy_id' => 0,
99 'corrections_policy_id' => 0,
100 'ethics_policy_id' => 0,
101 'diversity_policy_id' => 0,
102 'diversity_staffing_report_id' => 0,
103
104 'org-description' => '',
105 'org-email' => '',
106 'org-phone' => '',
107 'org-legal-name' => '',
108 'org-founding-date' => '',
109 'org-number-employees' => '',
110
111 'org-vat-id' => '',
112 'org-tax-id' => '',
113 'org-iso' => '',
114 'org-duns' => '',
115 'org-leicode' => '',
116 'org-naics' => '',
117
118 /*
119 * Uses enrich_defaults to add more along the lines of:
120 * - 'title-' . $pt->name => ''; // Text field.
121 * - 'metadesc-' . $pt->name => ''; // Text field.
122 * - 'noindex-' . $pt->name => false;
123 * - 'display-metabox-pt-' . $pt->name => false;
124 *
125 * - 'title-ptarchive-' . $pt->name => ''; // Text field.
126 * - 'metadesc-ptarchive-' . $pt->name => ''; // Text field.
127 * - 'bctitle-ptarchive-' . $pt->name => ''; // Text field.
128 * - 'noindex-ptarchive-' . $pt->name => false;
129 *
130 * - 'title-tax-' . $tax->name => '''; // Text field.
131 * - 'metadesc-tax-' . $tax->name => ''; // Text field.
132 * - 'noindex-tax-' . $tax->name => false;
133 * - 'display-metabox-tax-' . $tax->name => false;
134 *
135 * - 'schema-page-type-' . $pt->name => 'WebPage';
136 * - 'schema-article-type-' . $pt->name => 'Article';
137 */
138 ];
139
140 /**
141 * Used for "caching" during pageload.
142 *
143 * @var string[]|null
144 */
145 protected $enriched_defaults = null;
146
147 /**
148 * Array of variable option name patterns for the option.
149 *
150 * @var string[]
151 */
152 protected $variable_array_key_patterns = [
153 'title-',
154 'metadesc-',
155 'noindex-',
156 'display-metabox-pt-',
157 'bctitle-ptarchive-',
158 'post_types-',
159 'taxonomy-',
160 'schema-page-type-',
161 'schema-article-type-',
162 'social-title-',
163 'social-description-',
164 'social-image-url-',
165 'social-image-id-',
166 'org-',
167 ];
168
169 /**
170 * Array of sub-options which should not be overloaded with multi-site defaults.
171 *
172 * @var string[]
173 */
174 public $ms_exclude = [
175 'forcerewritetitle',
176 ];
177
178 /**
179 * Add the actions and filters for the option.
180 *
181 * @todo [JRF => testers] Check if the extra actions below would run into problems if an option
182 * is updated early on and if so, change the call to schedule these for a later action on add/update
183 * instead of running them straight away.
184 */
185 protected function __construct() {
186 parent::__construct();
187 add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_cache' ] );
188 add_action( 'init', [ $this, 'end_of_init' ], 999 );
189
190 add_action( 'registered_post_type', [ $this, 'invalidate_enrich_defaults_cache' ] );
191 add_action( 'unregistered_post_type', [ $this, 'invalidate_enrich_defaults_cache' ] );
192 add_action( 'registered_taxonomy', [ $this, 'invalidate_enrich_defaults_cache' ] );
193 add_action( 'unregistered_taxonomy', [ $this, 'invalidate_enrich_defaults_cache' ] );
194
195 add_filter( 'admin_title', [ 'Yoast_Input_Validation', 'add_yoast_admin_document_title_errors' ] );
196 }
197
198 /**
199 * Make sure we can recognize the right action for the double cleaning.
200 *
201 * @return void
202 */
203 public function end_of_init() {
204 do_action( 'wpseo_double_clean_titles' );
205 }
206
207 /**
208 * Get the singleton instance of this class.
209 *
210 * @return self
211 */
212 public static function get_instance() {
213 if ( ! ( self::$instance instanceof self ) ) {
214 self::$instance = new self();
215 }
216
217 return self::$instance;
218 }
219
220 /**
221 * Get the available separator options.
222 *
223 * @return string[]
224 */
225 public function get_separator_options() {
226 $separators = wp_list_pluck( self::get_separator_option_list(), 'option' );
227
228 /**
229 * Allow altering the array with separator options.
230 *
231 * @param array $separator_options Array with the separator options.
232 */
233 $filtered_separators = apply_filters( 'wpseo_separator_options', $separators );
234
235 if ( is_array( $filtered_separators ) && $filtered_separators !== [] ) {
236 $separators = array_merge( $separators, $filtered_separators );
237 }
238
239 return $separators;
240 }
241
242 /**
243 * Get the available separator options aria-labels.
244 *
245 * @return string[] Array with the separator options aria-labels.
246 */
247 public function get_separator_options_for_display() {
248 $separators = $this->get_separator_options();
249 $separator_list = self::get_separator_option_list();
250
251 $separator_options = [];
252
253 foreach ( $separators as $key => $label ) {
254 $aria_label = ( $separator_list[ $key ]['label'] ?? '' );
255
256 $separator_options[ $key ] = [
257 'label' => $label,
258 'aria_label' => $aria_label,
259 ];
260 }
261
262 return $separator_options;
263 }
264
265 /**
266 * Translate strings used in the option defaults.
267 *
268 * @return void
269 */
270 public function translate_defaults() {
271 /* translators: 1: Author name; 2: Site name. */
272 $this->defaults['title-author-wpseo'] = sprintf( __( '%1$s, Author at %2$s', 'wordpress-seo' ), '%%name%%', '%%sitename%%' ) . ' %%page%% ';
273 /* translators: %s expands to the search phrase. */
274 $this->defaults['title-search-wpseo'] = sprintf( __( 'You searched for %s', 'wordpress-seo' ), '%%searchphrase%%' ) . ' %%page%% %%sep%% %%sitename%%';
275 $this->defaults['title-404-wpseo'] = __( 'Page not found', 'wordpress-seo' ) . ' %%sep%% %%sitename%%';
276 /* translators: 1: link to post; 2: link to blog. */
277 $this->defaults['rssafter'] = sprintf( __( 'The post %1$s appeared first on %2$s.', 'wordpress-seo' ), '%%POSTLINK%%', '%%BLOGLINK%%' );
278
279 $this->defaults['breadcrumbs-404crumb'] = __( 'Error 404: Page not found', 'wordpress-seo' );
280 $this->defaults['breadcrumbs-archiveprefix'] = __( 'Archives for', 'wordpress-seo' );
281 $this->defaults['breadcrumbs-home'] = __( 'Home', 'wordpress-seo' );
282 $this->defaults['breadcrumbs-searchprefix'] = __( 'You searched for', 'wordpress-seo' );
283 }
284
285 /**
286 * Add dynamically created default options based on available post types and taxonomies.
287 *
288 * @return void
289 */
290 public function enrich_defaults() {
291 $enriched_defaults = $this->enriched_defaults;
292 if ( $enriched_defaults !== null ) {
293 $this->defaults += $enriched_defaults;
294 return;
295 }
296
297 $enriched_defaults = [];
298
299 /*
300 * Retrieve all the relevant post type and taxonomy arrays.
301 *
302 * WPSEO_Post_Type::get_accessible_post_types() should *not* be used here.
303 * These are the defaults and can be prepared for any public post type.
304 */
305 $post_type_objects = get_post_types( [ 'public' => true ], 'objects' );
306
307 if ( $post_type_objects ) {
308 /* translators: %s expands to the name of a post type (plural). */
309 $archive = sprintf( __( '%s Archive', 'wordpress-seo' ), '%%pt_plural%%' );
310
311 foreach ( $post_type_objects as $pt ) {
312 $enriched_defaults[ 'title-' . $pt->name ] = '%%title%% %%page%% %%sep%% %%sitename%%'; // Text field.
313 $enriched_defaults[ 'metadesc-' . $pt->name ] = ''; // Text area.
314 $enriched_defaults[ 'noindex-' . $pt->name ] = false;
315 $enriched_defaults[ 'display-metabox-pt-' . $pt->name ] = true;
316 $enriched_defaults[ 'post_types-' . $pt->name . '-maintax' ] = 0; // Select box.
317 $enriched_defaults[ 'schema-page-type-' . $pt->name ] = 'WebPage';
318 $enriched_defaults[ 'schema-article-type-' . $pt->name ] = ( $pt->name === 'post' ) ? 'Article' : 'None';
319
320 if ( $pt->name !== 'attachment' ) {
321 $enriched_defaults[ 'social-title-' . $pt->name ] = '%%title%%'; // Text field.
322 $enriched_defaults[ 'social-description-' . $pt->name ] = ''; // Text area.
323 $enriched_defaults[ 'social-image-url-' . $pt->name ] = ''; // Hidden input field.
324 $enriched_defaults[ 'social-image-id-' . $pt->name ] = 0; // Hidden input field.
325 }
326
327 // Custom post types that have archives.
328 if ( ! $pt->_builtin && WPSEO_Post_Type::has_archive( $pt ) ) {
329 $enriched_defaults[ 'title-ptarchive-' . $pt->name ] = $archive . ' %%page%% %%sep%% %%sitename%%'; // Text field.
330 $enriched_defaults[ 'metadesc-ptarchive-' . $pt->name ] = ''; // Text area.
331 $enriched_defaults[ 'bctitle-ptarchive-' . $pt->name ] = ''; // Text field.
332 $enriched_defaults[ 'noindex-ptarchive-' . $pt->name ] = false;
333 $enriched_defaults[ 'social-title-ptarchive-' . $pt->name ] = $archive; // Text field.
334 $enriched_defaults[ 'social-description-ptarchive-' . $pt->name ] = ''; // Text area.
335 $enriched_defaults[ 'social-image-url-ptarchive-' . $pt->name ] = ''; // Hidden input field.
336 $enriched_defaults[ 'social-image-id-ptarchive-' . $pt->name ] = 0; // Hidden input field.
337 }
338 }
339 }
340
341 $taxonomy_objects = get_taxonomies( [ 'public' => true ], 'object' );
342
343 if ( $taxonomy_objects ) {
344 /* translators: %s expands to the variable used for term title. */
345 $archives = sprintf( __( '%s Archives', 'wordpress-seo' ), '%%term_title%%' );
346
347 foreach ( $taxonomy_objects as $tax ) {
348 $enriched_defaults[ 'title-tax-' . $tax->name ] = $archives . ' %%page%% %%sep%% %%sitename%%'; // Text field.
349 $enriched_defaults[ 'metadesc-tax-' . $tax->name ] = ''; // Text area.
350 $enriched_defaults[ 'display-metabox-tax-' . $tax->name ] = true;
351
352 $enriched_defaults[ 'noindex-tax-' . $tax->name ] = ( $tax->name === 'post_format' );
353
354 $enriched_defaults[ 'social-title-tax-' . $tax->name ] = $archives; // Text field.
355 $enriched_defaults[ 'social-description-tax-' . $tax->name ] = ''; // Text area.
356 $enriched_defaults[ 'social-image-url-tax-' . $tax->name ] = ''; // Hidden input field.
357 $enriched_defaults[ 'social-image-id-tax-' . $tax->name ] = 0; // Hidden input field.
358
359 $enriched_defaults[ 'taxonomy-' . $tax->name . '-ptparent' ] = 0; // Select box;.
360 }
361 }
362
363 $this->enriched_defaults = $enriched_defaults;
364 $this->defaults += $enriched_defaults;
365 }
366
367 /**
368 * Invalidates enrich_defaults() cache.
369 *
370 * Called from actions:
371 * - (un)registered_post_type
372 * - (un)registered_taxonomy
373 *
374 * @return void
375 */
376 public function invalidate_enrich_defaults_cache() {
377 $this->enriched_defaults = null;
378 }
379
380 /**
381 * Validate the option.
382 *
383 * @param string[] $dirty New value for the option.
384 * @param string[] $clean Clean value for the option, normally the defaults.
385 * @param string[] $old Old value of the option.
386 *
387 * @return string[] Validated clean value for the option to be saved to the database.
388 */
389 protected function validate_option( $dirty, $clean, $old ) {
390 $allowed_post_types = $this->get_allowed_post_types();
391
392 foreach ( $clean as $key => $value ) {
393 $switch_key = $this->get_switch_key( $key );
394
395 switch ( $switch_key ) {
396 // Only ever set programmatically, so no reason for intense validation.
397 case 'company_logo_meta':
398 case 'person_logo_meta':
399 if ( isset( $dirty[ $key ] ) ) {
400 $clean[ $key ] = $dirty[ $key ];
401 }
402 break;
403
404 /* Breadcrumbs text fields. */
405 case 'breadcrumbs-404crumb':
406 case 'breadcrumbs-archiveprefix':
407 case 'breadcrumbs-home':
408 case 'breadcrumbs-prefix':
409 case 'breadcrumbs-searchprefix':
410 case 'breadcrumbs-sep':
411 if ( isset( $dirty[ $key ] ) ) {
412 $clean[ $key ] = wp_kses_post( $dirty[ $key ] );
413 }
414 break;
415
416 /*
417 * Text fields.
418 */
419
420 /*
421 * Covers:
422 * 'title-home-wpseo', 'title-author-wpseo', 'title-archive-wpseo', // phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- This isn't commented out code.
423 * 'title-search-wpseo', 'title-404-wpseo'
424 * 'title-' . $pt->name
425 * 'title-ptarchive-' . $pt->name
426 * 'title-tax-' . $tax->name
427 * 'social-title-' . $pt->name
428 * 'social-title-ptarchive-' . $pt->name
429 * 'social-title-tax-' . $tax->name
430 * 'social-title-author-wpseo', 'social-title-archive-wpseo'
431 * 'open_graph_frontpage_title'
432 */
433 case 'org-':
434 case 'website_name':
435 case 'alternate_website_name':
436 case 'title-':
437 case 'social-title-':
438 case 'open_graph_frontpage_title':
439 if ( isset( $dirty[ $key ] ) ) {
440 $clean[ $key ] = WPSEO_Utils::sanitize_text_field( $dirty[ $key ] );
441 }
442 break;
443
444 case 'company_or_person':
445 if ( isset( $dirty[ $key ] ) ) {
446 if ( in_array( $dirty[ $key ], [ 'company', 'person' ], true ) ) {
447 $clean[ $key ] = $dirty[ $key ];
448 }
449 else {
450 $defaults = $this->get_defaults();
451 $clean[ $key ] = $defaults['company_or_person'];
452 }
453 }
454 break;
455
456 /*
457 * Covers:
458 * 'company_logo', 'person_logo' // phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- This isn't commented out code.
459 */
460 case 'company_logo':
461 case 'person_logo':
462 case 'open_graph_frontpage_image':
463 // When a logo changes, we need to ditch the caches we have for it.
464 unset( $clean[ $switch_key . '_id' ] );
465 unset( $clean[ $switch_key . '_meta' ] );
466 $this->validate_url( $key, $dirty, $old, $clean );
467 break;
468
469 /*
470 * Covers:
471 * 'social-image-url-' . $pt->name
472 * 'social-image-url-ptarchive-' . $pt->name
473 * 'social-image-url-tax-' . $tax->name
474 * 'social-image-url-author-wpseo', 'social-image-url-archive-wpseo'
475 */
476 case 'social-image-url-':
477 $this->validate_url( $key, $dirty, $old, $clean );
478 break;
479
480 /*
481 * Covers:
482 * 'metadesc-home-wpseo', 'metadesc-author-wpseo', 'metadesc-archive-wpseo'
483 * 'metadesc-' . $pt->name
484 * 'metadesc-ptarchive-' . $pt->name
485 * 'metadesc-tax-' . $tax->name
486 * and also:
487 * 'bctitle-ptarchive-' . $pt->name
488 * 'social-description-' . $pt->name
489 * 'social-description-ptarchive-' . $pt->name
490 * 'social-description-tax-' . $tax->name
491 * 'social-description-author-wpseo', 'social-description-archive-wpseo'
492 * 'open_graph_frontpage_desc'
493 */
494 case 'metadesc-':
495 case 'bctitle-ptarchive-':
496 case 'company_name':
497 case 'company_alternate_name':
498 case 'person_name':
499 case 'social-description-':
500 case 'open_graph_frontpage_desc':
501 if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
502 $clean[ $key ] = WPSEO_Utils::sanitize_text_field( $dirty[ $key ] );
503 }
504 break;
505
506 /*
507 * Covers: 'rssbefore', 'rssafter' // phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- This isn't commented out code.
508 */
509 case 'rssbefore':
510 case 'rssafter':
511 if ( isset( $dirty[ $key ] ) ) {
512 $clean[ $key ] = wp_kses_post( $dirty[ $key ] );
513 }
514 break;
515
516 /* 'post_types-' . $pt->name . '-maintax' fields. */
517 case 'post_types-':
518 $post_type = str_replace( [ 'post_types-', '-maintax' ], '', $key );
519 $taxonomies = get_object_taxonomies( $post_type, 'names' );
520
521 if ( isset( $dirty[ $key ] ) ) {
522 if ( $taxonomies !== [] && in_array( $dirty[ $key ], $taxonomies, true ) ) {
523 $clean[ $key ] = $dirty[ $key ];
524 }
525 elseif ( (string) $dirty[ $key ] === '0' || (string) $dirty[ $key ] === '' ) {
526 $clean[ $key ] = 0;
527 }
528 elseif ( sanitize_title_with_dashes( $dirty[ $key ] ) === $dirty[ $key ] ) {
529 // Allow taxonomies which may not be registered yet.
530 $clean[ $key ] = $dirty[ $key ];
531 }
532 else {
533 if ( isset( $old[ $key ] ) ) {
534 $clean[ $key ] = sanitize_title_with_dashes( $old[ $key ] );
535 }
536
537 /*
538 * @todo [JRF => whomever] Maybe change the untranslated $pt name in the
539 * error message to the nicely translated label ?
540 */
541 add_settings_error(
542 $this->group_name, // Slug title of the setting.
543 $key, // Suffix-id for the error message box.
544 /* translators: %s expands to a post type. */
545 sprintf( __( 'Please select a valid taxonomy for post type "%s"', 'wordpress-seo' ), $post_type ), // The error message.
546 'error', // Message type.
547 );
548 }
549 }
550 elseif ( isset( $old[ $key ] ) ) {
551 $clean[ $key ] = sanitize_title_with_dashes( $old[ $key ] );
552 }
553 unset( $taxonomies, $post_type );
554 break;
555
556 /* 'taxonomy-' . $tax->name . '-ptparent' fields. */
557 case 'taxonomy-':
558 if ( isset( $dirty[ $key ] ) ) {
559 if ( $allowed_post_types !== [] && in_array( $dirty[ $key ], $allowed_post_types, true ) ) {
560 $clean[ $key ] = $dirty[ $key ];
561 }
562 elseif ( (string) $dirty[ $key ] === '0' || (string) $dirty[ $key ] === '' ) {
563 $clean[ $key ] = 0;
564 }
565 elseif ( sanitize_key( $dirty[ $key ] ) === $dirty[ $key ] ) {
566 // Allow taxonomies which may not be registered yet.
567 $clean[ $key ] = $dirty[ $key ];
568 }
569 else {
570 if ( isset( $old[ $key ] ) ) {
571 $clean[ $key ] = sanitize_key( $old[ $key ] );
572 }
573
574 /*
575 * @todo [JRF =? whomever] Maybe change the untranslated $tax name in the
576 * error message to the nicely translated label ?
577 */
578 $tax = str_replace( [ 'taxonomy-', '-ptparent' ], '', $key );
579 add_settings_error(
580 $this->group_name, // Slug title of the setting.
581 '_' . $tax, // Suffix-ID for the error message box.
582 /* translators: %s expands to a taxonomy slug. */
583 sprintf( __( 'Please select a valid post type for taxonomy "%s"', 'wordpress-seo' ), $tax ), // The error message.
584 'error', // Message type.
585 );
586 unset( $tax );
587 }
588 }
589 elseif ( isset( $old[ $key ] ) ) {
590 $clean[ $key ] = sanitize_key( $old[ $key ] );
591 }
592 break;
593
594 /*
595 * Covers:
596 * 'company_or_person_user_id'
597 * 'company_logo_id', 'person_logo_id', 'open_graph_frontpage_image_id'
598 * 'social-image-id-' . $pt->name
599 * 'social-image-id-ptarchive-' . $pt->name
600 * 'social-image-id-tax-' . $tax->name
601 * 'social-image-id-author-wpseo', 'social-image-id-archive-wpseo'
602 */
603 case 'company_or_person_user_id':
604 case 'company_logo_id':
605 case 'person_logo_id':
606 case 'social-image-id-':
607 case 'open_graph_frontpage_image_id':
608 case 'publishing_principles_id':
609 case 'ownership_funding_info_id':
610 case 'actionable_feedback_policy_id':
611 case 'corrections_policy_id':
612 case 'ethics_policy_id':
613 case 'diversity_policy_id':
614 case 'diversity_staffing_report_id':
615 if ( isset( $dirty[ $key ] ) ) {
616 $int = WPSEO_Utils::validate_int( $dirty[ $key ] );
617 if ( $int !== false && $int >= 0 ) {
618 $clean[ $key ] = $int;
619 }
620 }
621 elseif ( isset( $old[ $key ] ) ) {
622 $int = WPSEO_Utils::validate_int( $old[ $key ] );
623 if ( $int !== false && $int >= 0 ) {
624 $clean[ $key ] = $int;
625 }
626 }
627 break;
628 /* Separator field - Radio. */
629 case 'separator':
630 if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
631
632 // Get separator fields.
633 $separator_fields = $this->get_separator_options();
634
635 // Check if the given separator exists.
636 if ( isset( $separator_fields[ $dirty[ $key ] ] ) ) {
637 $clean[ $key ] = $dirty[ $key ];
638 }
639 }
640 break;
641
642 case 'schema-page-type-':
643 if ( isset( $dirty[ $key ] ) && is_string( $dirty[ $key ] ) ) {
644 if ( array_key_exists( $dirty[ $key ], Schema_Types::PAGE_TYPES ) ) {
645 $clean[ $key ] = $dirty[ $key ];
646 }
647 else {
648 $defaults = $this->get_defaults();
649 $post_type = str_replace( $switch_key, '', $key );
650 $clean[ $key ] = $defaults[ $switch_key . $post_type ];
651 }
652 }
653 break;
654 case 'schema-article-type-':
655 if ( isset( $dirty[ $key ] ) && is_string( $dirty[ $key ] ) ) {
656 /**
657 * Filter: 'wpseo_schema_article_types' - Allow developers to filter the available article types.
658 *
659 * Make sure when you filter this to also filter `wpseo_schema_article_types_labels`.
660 *
661 * @param array $schema_article_types The available schema article types.
662 */
663 if ( array_key_exists( $dirty[ $key ], apply_filters( 'wpseo_schema_article_types', Schema_Types::ARTICLE_TYPES ) ) ) {
664 $clean[ $key ] = $dirty[ $key ];
665 }
666 else {
667 $defaults = $this->get_defaults();
668 $post_type = str_replace( $switch_key, '', $key );
669 $clean[ $key ] = $defaults[ $switch_key . $post_type ];
670 }
671 }
672 break;
673
674 /*
675 * Boolean fields.
676 */
677
678 /*
679 * Covers:
680 * 'noindex-author-wpseo', 'noindex-author-noposts-wpseo', 'noindex-archive-wpseo'
681 * 'noindex-' . $pt->name
682 * 'noindex-ptarchive-' . $pt->name
683 * 'noindex-tax-' . $tax->name
684 * 'forcerewritetitle':
685 * 'noodp':
686 * 'noydir':
687 * 'disable-author':
688 * 'disable-date':
689 * 'disable-post_format';
690 * 'noindex-'
691 * 'display-metabox-pt-'
692 * 'display-metabox-pt-'. $pt->name
693 * 'display-metabox-tax-'
694 * 'display-metabox-tax-' . $tax->name
695 * 'breadcrumbs-display-blog-page'
696 * 'breadcrumbs-boldlast'
697 * 'breadcrumbs-enable'
698 * 'stripcategorybase'
699 */
700 default:
701 $clean[ $key ] = ( isset( $dirty[ $key ] ) ? WPSEO_Utils::validate_bool( $dirty[ $key ] ) : false );
702 break;
703 }
704 }
705
706 return $clean;
707 }
708
709 /**
710 * Retrieve a list of the allowed post types as breadcrumb parent for a taxonomy.
711 * Helper method for validation.
712 *
713 * {@internal Don't make static as new types may still be registered.}}
714 *
715 * @return string[]
716 */
717 protected function get_allowed_post_types() {
718 $allowed_post_types = [];
719
720 /*
721 * WPSEO_Post_Type::get_accessible_post_types() should *not* be used here.
722 */
723 $post_types = get_post_types( [ 'public' => true ], 'objects' );
724
725 if ( get_option( 'show_on_front' ) === 'page' && get_option( 'page_for_posts' ) > 0 ) {
726 $allowed_post_types[] = 'post';
727 }
728
729 if ( is_array( $post_types ) && $post_types !== [] ) {
730 foreach ( $post_types as $type ) {
731 if ( WPSEO_Post_Type::has_archive( $type ) ) {
732 $allowed_post_types[] = $type->name;
733 }
734 }
735 }
736
737 return $allowed_post_types;
738 }
739
740 /**
741 * Clean a given option value.
742 *
743 * @param string[] $option_value Old (not merged with defaults or filtered) option value to clean according to the rules for this option.
744 * @param string[]|null $current_version Optional. Version from which to upgrade, if not set, version specific upgrades will be disregarded.
745 * @param string[]|null $all_old_option_values Optional. Only used when importing old options to have access to the real old values, in contrast to the saved ones.
746 *
747 * @return string[] Cleaned option.
748 */
749 protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) {
750 static $original = null;
751
752 // Double-run this function to ensure renaming of the taxonomy options will work.
753 if ( ! isset( $original )
754 && has_action( 'wpseo_double_clean_titles', [ $this, 'clean' ] ) === false
755 ) {
756 add_action( 'wpseo_double_clean_titles', [ $this, 'clean' ] );
757 $original = $option_value;
758 }
759
760 /*
761 * Move options from very old option to this one.
762 *
763 * {@internal Don't rename to the 'current' names straight away as that would prevent
764 * the rename/unset combi below from working.}}
765 *
766 * @todo [JRF] Maybe figure out a smarter way to deal with this.
767 */
768 $old_option = null;
769 if ( isset( $all_old_option_values ) ) {
770 // Ok, we have an import.
771 if ( isset( $all_old_option_values['wpseo_indexation'] ) && is_array( $all_old_option_values['wpseo_indexation'] ) && $all_old_option_values['wpseo_indexation'] !== [] ) {
772 $old_option = $all_old_option_values['wpseo_indexation'];
773 }
774 }
775 else {
776 $old_option = get_option( 'wpseo_indexation' );
777 }
778 if ( is_array( $old_option ) && $old_option !== [] ) {
779 $move = [
780 'noindexauthor' => 'noindex-author',
781 'disableauthor' => 'disable-author',
782 'noindexdate' => 'noindex-archive',
783 'noindexcat' => 'noindex-category',
784 'noindextag' => 'noindex-post_tag',
785 'noindexpostformat' => 'noindex-post_format',
786 ];
787 foreach ( $move as $old => $new ) {
788 if ( isset( $old_option[ $old ] ) && ! isset( $option_value[ $new ] ) ) {
789 $option_value[ $new ] = $old_option[ $old ];
790 }
791 }
792 unset( $move, $old, $new );
793 }
794 unset( $old_option );
795
796 // Fix wrongness created by buggy version 1.2.2.
797 if ( isset( $option_value['title-home'] ) && $option_value['title-home'] === '%%sitename%% - %%sitedesc%% - 12345' ) {
798 $option_value['title-home-wpseo'] = '%%sitename%% - %%sitedesc%%';
799 }
800
801 /*
802 * Renaming these options to avoid ever overwritting these if a (bloody stupid) user /
803 * programmer would use any of the following as a custom post type or custom taxonomy:
804 * 'home', 'author', 'archive', 'search', '404', 'subpages'.
805 *
806 * Similarly, renaming the tax options to avoid a custom post type and a taxonomy
807 * with the same name occupying the same option.
808 */
809 $rename = [
810 'title-home' => 'title-home-wpseo',
811 'title-author' => 'title-author-wpseo',
812 'title-archive' => 'title-archive-wpseo',
813 'title-search' => 'title-search-wpseo',
814 'title-404' => 'title-404-wpseo',
815 'metadesc-home' => 'metadesc-home-wpseo',
816 'metadesc-author' => 'metadesc-author-wpseo',
817 'metadesc-archive' => 'metadesc-archive-wpseo',
818 'noindex-author' => 'noindex-author-wpseo',
819 'noindex-archive' => 'noindex-archive-wpseo',
820 ];
821 foreach ( $rename as $old => $new ) {
822 if ( isset( $option_value[ $old ] ) && ! isset( $option_value[ $new ] ) ) {
823 $option_value[ $new ] = $option_value[ $old ];
824 unset( $option_value[ $old ] );
825 }
826 }
827 unset( $rename, $old, $new );
828
829 /*
830 * {@internal This clean-up action can only be done effectively once the taxonomies
831 * and post_types have been registered, i.e. at the end of the init action.}}
832 */
833 if ( ( isset( $original ) && current_filter() === 'wpseo_double_clean_titles' ) || did_action( 'wpseo_double_clean_titles' ) > 0 ) {
834 $rename = [
835 'title-' => 'title-tax-',
836 'metadesc-' => 'metadesc-tax-',
837 'noindex-' => 'noindex-tax-',
838 'tax-hideeditbox-' => 'hideeditbox-tax-',
839 ];
840
841 $taxonomy_names = get_taxonomies( [ 'public' => true ], 'names' );
842 $post_type_names = get_post_types( [ 'public' => true ], 'names' );
843 $defaults = $this->get_defaults();
844 if ( $taxonomy_names !== [] ) {
845 foreach ( $taxonomy_names as $tax ) {
846 foreach ( $rename as $old_prefix => $new_prefix ) {
847 if (
848 ( isset( $original[ $old_prefix . $tax ] ) && ! isset( $original[ $new_prefix . $tax ] ) )
849 && ( ! isset( $option_value[ $new_prefix . $tax ] )
850 || ( isset( $option_value[ $new_prefix . $tax ] )
851 && $option_value[ $new_prefix . $tax ] === $defaults[ $new_prefix . $tax ] ) )
852 ) {
853 $option_value[ $new_prefix . $tax ] = $original[ $old_prefix . $tax ];
854
855 /*
856 * Check if there is a cpt with the same name as the tax,
857 * if so, we should make sure that the old setting hasn't been removed.
858 */
859 if ( ! isset( $post_type_names[ $tax ] ) && isset( $option_value[ $old_prefix . $tax ] ) ) {
860 unset( $option_value[ $old_prefix . $tax ] );
861 }
862 elseif ( isset( $post_type_names[ $tax ] ) && ! isset( $option_value[ $old_prefix . $tax ] ) ) {
863 $option_value[ $old_prefix . $tax ] = $original[ $old_prefix . $tax ];
864 }
865
866 if ( $old_prefix === 'tax-hideeditbox-' ) {
867 unset( $option_value[ $old_prefix . $tax ] );
868 }
869 }
870 }
871 }
872 }
873 unset( $rename, $taxonomy_names, $post_type_names, $defaults, $tax, $old_prefix, $new_prefix );
874 }
875
876 return $option_value;
877 }
878
879 /**
880 * Make sure that any set option values relating to post_types and/or taxonomies are retained,
881 * even when that post_type or taxonomy may not yet have been registered.
882 *
883 * {@internal Overrule the abstract class version of this to make sure one extra renamed
884 * variable key does not get removed. IMPORTANT: keep this method in line with
885 * the parent on which it is based!}}
886 *
887 * @param string[] $dirty Original option as retrieved from the database.
888 * @param string[] $clean Filtered option where any options which shouldn't be in our option
889 * have already been removed and any options which weren't set
890 * have been set to their defaults.
891 *
892 * @return string[]
893 */
894 protected function retain_variable_keys( $dirty, $clean ) {
895 if ( ( is_array( $this->variable_array_key_patterns ) && $this->variable_array_key_patterns !== [] ) && ( is_array( $dirty ) && $dirty !== [] ) ) {
896
897 // Add the extra pattern.
898 $patterns = $this->variable_array_key_patterns;
899 $patterns[] = 'tax-hideeditbox-';
900
901 /**
902 * Allow altering the array with variable array key patterns.
903 *
904 * @param array $patterns Array with the variable array key patterns.
905 */
906 $patterns = apply_filters( 'wpseo_option_titles_variable_array_key_patterns', $patterns );
907
908 foreach ( $dirty as $key => $value ) {
909
910 // Do nothing if already in filtered option array.
911 if ( isset( $clean[ $key ] ) ) {
912 continue;
913 }
914
915 foreach ( $patterns as $pattern ) {
916 if ( strpos( $key, $pattern ) === 0 ) {
917 $clean[ $key ] = $value;
918 break;
919 }
920 }
921 }
922 }
923
924 return $clean;
925 }
926
927 /**
928 * Retrieves a list of separator options.
929 *
930 * @return string[] An array of the separator options.
931 */
932 protected static function get_separator_option_list() {
933 $separators = [
934 'sc-dash' => [
935 'option' => '-',
936 'label' => __( 'Dash', 'wordpress-seo' ),
937 ],
938 'sc-ndash' => [
939 'option' => '&#8211;',
940 'label' => __( 'En dash', 'wordpress-seo' ),
941 ],
942 'sc-mdash' => [
943 'option' => '&#8212;',
944 'label' => __( 'Em dash', 'wordpress-seo' ),
945 ],
946 'sc-colon' => [
947 'option' => ':',
948 'label' => __( 'Colon', 'wordpress-seo' ),
949 ],
950 'sc-middot' => [
951 'option' => '&#183;',
952 'label' => __( 'Middle dot', 'wordpress-seo' ),
953 ],
954 'sc-bull' => [
955 'option' => '&#8226;',
956 'label' => __( 'Bullet', 'wordpress-seo' ),
957 ],
958 'sc-star' => [
959 'option' => '*',
960 'label' => __( 'Asterisk', 'wordpress-seo' ),
961 ],
962 'sc-smstar' => [
963 'option' => '&#8902;',
964 'label' => __( 'Low asterisk', 'wordpress-seo' ),
965 ],
966 'sc-pipe' => [
967 'option' => '|',
968 'label' => __( 'Vertical bar', 'wordpress-seo' ),
969 ],
970 'sc-tilde' => [
971 'option' => '~',
972 'label' => __( 'Small tilde', 'wordpress-seo' ),
973 ],
974 'sc-laquo' => [
975 'option' => '&#171;',
976 'label' => __( 'Left angle quotation mark', 'wordpress-seo' ),
977 ],
978 'sc-raquo' => [
979 'option' => '&#187;',
980 'label' => __( 'Right angle quotation mark', 'wordpress-seo' ),
981 ],
982 'sc-lt' => [
983 'option' => '&#062;',
984 'label' => __( 'Less than sign', 'wordpress-seo' ),
985 ],
986 'sc-gt' => [
987 'option' => '&#060;',
988 'label' => __( 'Greater than sign', 'wordpress-seo' ),
989 ],
990 ];
991
992 /**
993 * Allows altering the separator options array.
994 *
995 * @param array $separators Array with the separator options.
996 */
997 $separator_list = apply_filters( 'wpseo_separator_option_list', $separators );
998
999 if ( ! is_array( $separator_list ) ) {
1000 return $separators;
1001 }
1002
1003 return $separator_list;
1004 }
1005 }
1006