PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 18.6
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v18.6
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 18.6, at inc/options/class-wpseo-option-titles.php

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