PluginProbe ʕ •ᴥ•ʔ
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.1
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.1
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 19.1 19.10 19.11 19.12 19.13 19.14 19.2 19.3 19.4 19.5 19.5.1 19.6 19.6.1 19.7 19.7.1 19.7.2 19.8 19.9 20.0 20.1 20.10 20.11 20.12 20.13 20.2 20.2.1 20.3 20.4 20.5 20.6 20.7 20.8 20.9 21.0 21.1 21.2 21.3 21.4 21.5 21.6 21.7 21.8 21.8.1 21.9 21.9.1 22.0 22.1 22.2 22.3 22.4 22.5 22.6 22.7 22.8 22.9 23.0 23.1 23.2 23.3 23.4 23.5 23.6 23.7 23.8 23.9 24.0 24.1 24.2 24.3 24.4 24.5 24.6 24.7 24.8 24.8.1 24.9 25.0 25.1 25.2 25.3 25.3.1 25.4 25.5 25.6 25.7 25.8 25.9 26.0 26.1 26.1.1 26.2 26.3 26.4 26.5 26.6 26.7 26.8 26.9 27.0 27.1 27.1.1 27.2 27.3 27.4
wordpress-seo / inc / class-wpseo-meta.php
wordpress-seo / inc Last commit date
exceptions 5 years ago options 3 months ago sitemaps 3 months ago class-addon-manager.php 3 months ago class-my-yoast-api-request.php 3 months ago class-post-type.php 1 year ago class-rewrite.php 3 months ago class-upgrade-history.php 3 months ago class-upgrade.php 3 months ago class-wpseo-admin-bar-menu.php 3 months ago class-wpseo-content-images.php 3 months ago class-wpseo-custom-fields.php 1 year ago class-wpseo-custom-taxonomies.php 3 months ago class-wpseo-image-utils.php 3 months ago class-wpseo-installation.php 7 months ago class-wpseo-meta.php 3 months ago class-wpseo-primary-term.php 2 years ago class-wpseo-rank.php 3 months ago class-wpseo-replace-vars.php 3 months ago class-wpseo-replacement-variable.php 5 years ago class-wpseo-shortlinker.php 2 years ago class-wpseo-statistics.php 5 years ago class-wpseo-utils.php 3 months ago class-yoast-dynamic-rewrites.php 2 years ago date-helper.php 5 years ago index.php 10 years ago interface-wpseo-wordpress-ajax-integration.php 7 years ago interface-wpseo-wordpress-integration.php 7 years ago language-utils.php 2 years ago wpseo-functions-deprecated.php 2 years ago wpseo-functions.php 2 years ago wpseo-non-ajax-functions.php 5 years ago
class-wpseo-meta.php
1046 lines
1 <?php
2 /**
3 * WPSEO plugin file.
4 *
5 * @package WPSEO\Internals
6 * @since 1.5.0
7 */
8
9 use Yoast\WP\SEO\Config\Schema_Types;
10 use Yoast\WP\SEO\Helpers\Schema\Article_Helper;
11 use Yoast\WP\SEO\Repositories\Indexable_Repository;
12
13 /**
14 * This class implements defaults and value validation for all WPSEO Post Meta values.
15 *
16 * Some guidelines:
17 * - To update a meta value, you can just use update_post_meta() with the full (prefixed) meta key
18 * or the convenience method WPSEO_Meta::set_value() with the internal key.
19 * All updates will be automatically validated.
20 * Meta values will only be saved to the database if they are *not* the same as the default to
21 * keep database load low.
22 * - To retrieve a WPSEO meta value, you **must** use WPSEO_Meta::get_value() which will always return a
23 * string value, either the saved value or the default.
24 * This method can also retrieve a complete set of WPSEO meta values for one specific post, see
25 * the method documentation for the parameters.
26 *
27 * {@internal Unfortunately there isn't a filter available to hook into before returning the results
28 * for get_post_meta(), get_post_custom() and the likes. That would have been the
29 * preferred solution.}}
30 *
31 * {@internal All WP native get_meta() results get cached internally, so no need to cache locally.}}
32 * {@internal Use $key when the key is the WPSEO internal name (without prefix), $meta_key when it
33 * includes the prefix.}}
34 */
35 class WPSEO_Meta {
36
37 /**
38 * Prefix for all WPSEO meta values in the database.
39 *
40 * {@internal If at any point this would change, quite apart from an upgrade routine,
41 * this also will need to be changed in the wpml-config.xml file.}}
42 *
43 * @var string
44 */
45 public static $meta_prefix = '_yoast_wpseo_';
46
47 /**
48 * Prefix for all WPSEO meta value form field names and ids.
49 *
50 * @var string
51 */
52 public static $form_prefix = 'yoast_wpseo_';
53
54 /**
55 * Allowed length of the meta description.
56 *
57 * @var int
58 */
59 public static $meta_length = 156;
60
61 /**
62 * Reason the meta description is not the default length.
63 *
64 * @var string
65 */
66 public static $meta_length_reason = '';
67
68 /**
69 * Meta box field definitions for the meta box form.
70 *
71 * {@internal
72 * - Titles, help texts, description text and option labels are added via a translate_meta_boxes() method
73 * in the relevant child classes (WPSEO_Metabox and WPSEO_Social_admin) as they are only needed there.
74 * - Beware: even though the meta keys are divided into subsets, they still have to be uniquely named!}}
75 *
76 * @var array
77 * Array format:
78 * (required) 'type' => (string) field type. i.e. text / textarea / checkbox /
79 * radio / select / multiselect / upload etc.
80 * (recommended) 'default_value' => (string|array) default value for the field.
81 * IMPORTANT:
82 * - if the field has options, the default has to be the
83 * key of one of the options.
84 * - if the field is a text field, the default **has** to be
85 * an empty string as otherwise the user can't save
86 * an empty value/delete the meta value.
87 * - if the field is a checkbox, the only valid values
88 * are 'on' or 'off'.
89 * (semi-required) 'options' => (array) options for used with (multi-)select and radio
90 * fields, required if that's the field type.
91 * key = (string) value which will be saved to db.
92 * value = (string) text label for the option.
93 * (optional) 'autocomplete' => (bool) whether autocomplete is on for text fields,
94 * defaults to true.
95 * (optional) 'class' => (string) classname(s) to add to the actual <input> tag.
96 * (optional) 'rows' => (int) number of rows for a textarea, defaults to 3.
97 * (optional) 'serialized' => (bool) whether the value is expected to be serialized,
98 * i.e. an array or object, defaults to false.
99 * Currently only used by add-on plugins.
100 */
101 public static $meta_fields = [
102 'general' => [
103 'focuskw' => [
104 'type' => 'hidden',
105 'title' => '',
106 ],
107 'title' => [
108 'type' => 'hidden',
109 'default_value' => '',
110 ],
111 'metadesc' => [
112 'type' => 'hidden',
113 'default_value' => '',
114 'class' => 'metadesc',
115 'rows' => 2,
116 ],
117 'linkdex' => [
118 'type' => 'hidden',
119 'default_value' => '0',
120 ],
121 'content_score' => [
122 'type' => 'hidden',
123 'default_value' => '0',
124 ],
125 'inclusive_language_score' => [
126 'type' => 'hidden',
127 'default_value' => '0',
128 ],
129 'is_cornerstone' => [
130 'type' => 'hidden',
131 'default_value' => 'false',
132 ],
133 ],
134 'advanced' => [
135 'meta-robots-noindex' => [
136 'type' => 'hidden',
137 'default_value' => '0', // = post-type default.
138 'options' => [
139 '0' => '', // Post type default.
140 '2' => '', // Index.
141 '1' => '', // No-index.
142 ],
143 ],
144 'meta-robots-nofollow' => [
145 'type' => 'hidden',
146 'default_value' => '0', // = follow.
147 'options' => [
148 '0' => '', // Follow.
149 '1' => '', // No-follow.
150 ],
151 ],
152 'meta-robots-adv' => [
153 'type' => 'hidden',
154 'default_value' => '',
155 'options' => [
156 'noimageindex' => '',
157 'noarchive' => '',
158 'nosnippet' => '',
159 ],
160 ],
161 'bctitle' => [
162 'type' => 'hidden',
163 'default_value' => '',
164 ],
165 'canonical' => [
166 'type' => 'hidden',
167 'default_value' => '',
168 ],
169 'redirect' => [
170 'type' => 'url',
171 'default_value' => '',
172 ],
173 ],
174 'social' => [],
175 'schema' => [
176 'schema_page_type' => [
177 'type' => 'hidden',
178 'options' => Schema_Types::PAGE_TYPES,
179 ],
180 'schema_article_type' => [
181 'type' => 'hidden',
182 'hide_on_pages' => true,
183 'options' => Schema_Types::ARTICLE_TYPES,
184 ],
185 ],
186 /* Fields we should validate & save, but not show on any form. */
187 'non_form' => [
188 'linkdex' => [
189 'type' => null,
190 'default_value' => '0',
191 ],
192 ],
193 ];
194
195 /**
196 * Helper property - reverse index of the definition array.
197 *
198 * Format: [full meta key including prefix] => array
199 * ['subset'] => (string) primary index
200 * ['key'] => (string) internal key
201 *
202 * @var array
203 */
204 public static $fields_index = [];
205
206 /**
207 * Helper property - array containing only the defaults in the format:
208 * [full meta key including prefix] => (string) default value
209 *
210 * @var array
211 */
212 public static $defaults = [];
213
214 /**
215 * Helper property to define the social network meta field definitions - networks.
216 *
217 * @var array
218 */
219 private static $social_networks = [
220 'opengraph' => 'opengraph',
221 'twitter' => 'twitter',
222 ];
223
224 /**
225 * Helper property to define the social network meta field definitions - fields and their type.
226 *
227 * @var array
228 */
229 private static $social_fields = [
230 'title' => 'hidden',
231 'description' => 'hidden',
232 'image' => 'hidden',
233 'image-id' => 'hidden',
234 ];
235
236 /**
237 * Register our actions and filters.
238 *
239 * @return void
240 */
241 public static function init() {
242 foreach ( self::$social_networks as $option => $network ) {
243 if ( WPSEO_Options::get( $option, false, [ 'wpseo_social' ] ) === true ) {
244 foreach ( self::$social_fields as $box => $type ) {
245 self::$meta_fields['social'][ $network . '-' . $box ] = [
246 'type' => $type,
247 'default_value' => '',
248 ];
249 }
250 }
251 }
252 unset( $option, $network, $box, $type );
253
254 /**
255 * Allow add-on plugins to register their meta fields for management by this class.
256 * Calls to add_filter() must be made before plugins_loaded prio 14.
257 */
258 $extra_fields = apply_filters( 'add_extra_wpseo_meta_fields', [] );
259 if ( is_array( $extra_fields ) ) {
260 self::$meta_fields = self::array_merge_recursive_distinct( $extra_fields, self::$meta_fields );
261 }
262 unset( $extra_fields );
263
264 foreach ( self::$meta_fields as $subset => $field_group ) {
265 foreach ( $field_group as $key => $field_def ) {
266
267 register_meta(
268 'post',
269 self::$meta_prefix . $key,
270 [ 'sanitize_callback' => [ self::class, 'sanitize_post_meta' ] ],
271 );
272
273 // Set the $fields_index property for efficiency.
274 self::$fields_index[ self::$meta_prefix . $key ] = [
275 'subset' => $subset,
276 'key' => $key,
277 ];
278
279 // Set the $defaults property for efficiency.
280 if ( isset( $field_def['default_value'] ) ) {
281 self::$defaults[ self::$meta_prefix . $key ] = $field_def['default_value'];
282 }
283 else {
284 // Meta will always be a string, so let's make the meta meta default also a string.
285 self::$defaults[ self::$meta_prefix . $key ] = '';
286 }
287 }
288 }
289 unset( $subset, $field_group, $key, $field_def );
290
291 self::filter_schema_article_types();
292
293 add_filter( 'update_post_metadata', [ self::class, 'remove_meta_if_default' ], 10, 5 );
294 add_filter( 'add_post_metadata', [ self::class, 'dont_save_meta_if_default' ], 10, 4 );
295 }
296
297 /**
298 * Retrieve the meta box form field definitions for the given tab and post type.
299 *
300 * @param string $tab Tab for which to retrieve the field definitions.
301 * @param string $post_type Post type of the current post.
302 *
303 * @return array Array containing the meta box field definitions.
304 */
305 public static function get_meta_field_defs( $tab, $post_type = 'post' ) {
306 if ( ! isset( self::$meta_fields[ $tab ] ) ) {
307 return [];
308 }
309
310 $field_defs = self::$meta_fields[ $tab ];
311
312 switch ( $tab ) {
313 case 'non-form':
314 // Prevent non-form fields from being passed to forms.
315 $field_defs = [];
316 break;
317
318 case 'advanced':
319 global $post;
320
321 if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_edit_advanced_metadata' ) && WPSEO_Options::get( 'disableadvanced_meta' ) ) {
322 return [];
323 }
324
325 $post_type = '';
326 if ( isset( $post->post_type ) ) {
327 $post_type = $post->post_type;
328 }
329 elseif ( ! isset( $post->post_type ) && isset( $_GET['post_type'] ) ) {
330 $post_type = sanitize_text_field( $_GET['post_type'] );
331 }
332
333 if ( $post_type === '' ) {
334 return [];
335 }
336
337 /* Don't show the breadcrumb title field if breadcrumbs aren't enabled. */
338 if ( WPSEO_Options::get( 'breadcrumbs-enable', false ) !== true && ! current_theme_supports( 'yoast-seo-breadcrumbs' ) ) {
339 unset( $field_defs['bctitle'] );
340 }
341
342 if ( empty( $post->ID ) || ( ! empty( $post->ID ) && self::get_value( 'redirect', $post->ID ) === '' ) ) {
343 unset( $field_defs['redirect'] );
344 }
345 break;
346
347 case 'schema':
348 if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_edit_advanced_metadata' ) && WPSEO_Options::get( 'disableadvanced_meta' ) ) {
349 return [];
350 }
351
352 $field_defs['schema_page_type']['default'] = WPSEO_Options::get( 'schema-page-type-' . $post_type );
353
354 $article_helper = new Article_Helper();
355 if ( $article_helper->is_article_post_type( $post_type ) ) {
356 $default_schema_article_type = WPSEO_Options::get( 'schema-article-type-' . $post_type );
357
358 /** This filter is documented in inc/options/class-wpseo-option-titles.php */
359 $allowed_article_types = apply_filters( 'wpseo_schema_article_types', Schema_Types::ARTICLE_TYPES );
360
361 if ( ! array_key_exists( $default_schema_article_type, $allowed_article_types ) ) {
362 $default_schema_article_type = WPSEO_Options::get_default( 'wpseo_titles', 'schema-article-type-' . $post_type );
363 }
364 $field_defs['schema_article_type']['default'] = $default_schema_article_type;
365 }
366 else {
367 unset( $field_defs['schema_article_type'] );
368 }
369
370 break;
371 }
372
373 /**
374 * Filter the WPSEO metabox form field definitions for a tab.
375 * {tab} can be 'general', 'advanced' or 'social'.
376 *
377 * @param array $field_defs Metabox form field definitions.
378 * @param string $post_type Post type of the post the metabox is for, defaults to 'post'.
379 *
380 * @return array
381 */
382 return apply_filters( 'wpseo_metabox_entries_' . $tab, $field_defs, $post_type );
383 }
384
385 /**
386 * Validate the post meta values.
387 *
388 * @param mixed $meta_value The new value.
389 * @param string $meta_key The full meta key (including prefix).
390 *
391 * @return string Validated meta value.
392 */
393 public static function sanitize_post_meta( $meta_value, $meta_key ) {
394 $field_def = self::$meta_fields[ self::$fields_index[ $meta_key ]['subset'] ][ self::$fields_index[ $meta_key ]['key'] ];
395 $clean = self::$defaults[ $meta_key ];
396
397 switch ( true ) {
398 case ( $meta_key === self::$meta_prefix . 'linkdex' ):
399 $int = WPSEO_Utils::validate_int( $meta_value );
400 if ( $int !== false && $int >= 0 ) {
401 $clean = (string) $int; // Convert to string to make sure default check works.
402 }
403 break;
404
405 case ( $field_def['type'] === 'checkbox' ):
406 // Only allow value if it's one of the predefined options.
407 if ( in_array( $meta_value, [ 'on', 'off' ], true ) ) {
408 $clean = $meta_value;
409 }
410 break;
411
412 case ( $field_def['type'] === 'select' || $field_def['type'] === 'radio' ):
413 // Only allow value if it's one of the predefined options.
414 if ( isset( $field_def['options'][ $meta_value ] ) ) {
415 $clean = $meta_value;
416 }
417 break;
418
419 case ( $field_def['type'] === 'hidden' && $meta_key === self::$meta_prefix . 'meta-robots-adv' ):
420 $clean = self::validate_meta_robots_adv( $meta_value );
421 break;
422
423 case ( $field_def['type'] === 'url' || $meta_key === self::$meta_prefix . 'canonical' ):
424 // Validate as url(-part).
425 $url = WPSEO_Utils::sanitize_url( $meta_value );
426 if ( $url !== '' ) {
427 $clean = $url;
428 }
429 break;
430
431 case ( $field_def['type'] === 'upload' && in_array( $meta_key, [ self::$meta_prefix . 'opengraph-image', self::$meta_prefix . 'twitter-image' ], true ) ):
432 // Validate as url.
433 $url = WPSEO_Utils::sanitize_url( $meta_value, [ 'http', 'https', 'ftp', 'ftps' ] );
434 if ( $url !== '' ) {
435 $clean = $url;
436 }
437 break;
438
439 case ( $field_def['type'] === 'hidden' && $meta_key === self::$meta_prefix . 'is_cornerstone' ):
440 $clean = $meta_value;
441
442 /*
443 * This used to be a checkbox, then became a hidden input.
444 * To make sure the value remains consistent, we cast 'true' to '1'.
445 */
446 if ( $meta_value === 'true' ) {
447 $clean = '1';
448 }
449 break;
450
451 case ( $field_def['type'] === 'hidden' && isset( $field_def['options'] ) ):
452 // Only allow value if it's one of the predefined options.
453 if ( isset( $field_def['options'][ $meta_value ] ) ) {
454 $clean = $meta_value;
455 }
456 break;
457
458 case ( $field_def['type'] === 'textarea' ):
459 if ( is_string( $meta_value ) ) {
460 // Remove line breaks and tabs.
461 // @todo [JRF => Yoast] Verify that line breaks and the likes aren't allowed/recommended in meta header fields.
462 $meta_value = str_replace( [ "\n", "\r", "\t", ' ' ], ' ', $meta_value );
463 $clean = WPSEO_Utils::sanitize_text_field( trim( $meta_value ) );
464 }
465 break;
466
467 case ( $field_def['type'] === 'multiselect' ):
468 $clean = $meta_value;
469 break;
470
471 case ( $field_def['type'] === 'text' ):
472 default:
473 if ( is_string( $meta_value ) ) {
474 $clean = WPSEO_Utils::sanitize_text_field( trim( $meta_value ) );
475 }
476
477 break;
478 }
479
480 $clean = apply_filters( 'wpseo_sanitize_post_meta_' . $meta_key, $clean, $meta_value, $field_def, $meta_key );
481
482 return $clean;
483 }
484
485 /**
486 * Validate a meta-robots-adv meta value.
487 *
488 * @todo [JRF => Yoast] Verify that this logic for the prioritisation is correct.
489 *
490 * @param array|string $meta_value The value to validate.
491 *
492 * @return string Clean value.
493 */
494 public static function validate_meta_robots_adv( $meta_value ) {
495 $clean = self::$meta_fields['advanced']['meta-robots-adv']['default_value'];
496 $options = self::$meta_fields['advanced']['meta-robots-adv']['options'];
497
498 if ( is_string( $meta_value ) ) {
499 $meta_value = explode( ',', $meta_value );
500 }
501
502 if ( is_array( $meta_value ) && $meta_value !== [] ) {
503 $meta_value = array_map( 'trim', $meta_value );
504
505 // Individual selected entries.
506 $cleaning = [];
507 foreach ( $meta_value as $value ) {
508 if ( isset( $options[ $value ] ) ) {
509 $cleaning[] = $value;
510 }
511 }
512
513 if ( $cleaning !== [] ) {
514 $clean = implode( ',', $cleaning );
515 }
516 unset( $cleaning, $value );
517 }
518
519 return $clean;
520 }
521
522 /**
523 * Prevent saving of default values and remove potential old value from the database if replaced by a default.
524 *
525 * @param bool $check The current status to allow updating metadata for the given type.
526 * @param int $object_id ID of the current object for which the meta is being updated.
527 * @param string $meta_key The full meta key (including prefix).
528 * @param string $meta_value New meta value.
529 * @param string $prev_value The old meta value.
530 *
531 * @return bool|null True = stop saving, null = continue saving.
532 */
533 public static function remove_meta_if_default( $check, $object_id, $meta_key, $meta_value, $prev_value = '' ) {
534 /* If it's one of our meta fields, check against default. */
535 if ( isset( self::$fields_index[ $meta_key ] ) && self::meta_value_is_default( $meta_key, $meta_value ) === true ) {
536 if ( $prev_value !== '' ) {
537 delete_post_meta( $object_id, $meta_key, $prev_value );
538 }
539 else {
540 delete_post_meta( $object_id, $meta_key );
541 }
542
543 return true; // Stop saving the value.
544 }
545
546 return $check; // Go on with the normal execution (update) in meta.php.
547 }
548
549 /**
550 * Prevent adding of default values to the database.
551 *
552 * @param bool $check The current status to allow adding metadata for the given type.
553 * @param int $object_id ID of the current object for which the meta is being added.
554 * @param string $meta_key The full meta key (including prefix).
555 * @param string $meta_value New meta value.
556 *
557 * @return bool|null True = stop saving, null = continue saving.
558 */
559 public static function dont_save_meta_if_default( $check, $object_id, $meta_key, $meta_value ) {
560 /* If it's one of our meta fields, check against default. */
561 if ( isset( self::$fields_index[ $meta_key ] ) && self::meta_value_is_default( $meta_key, $meta_value ) === true ) {
562 return true; // Stop saving the value.
563 }
564
565 return $check; // Go on with the normal execution (add) in meta.php.
566 }
567
568 /**
569 * Is the given meta value the same as the default value ?
570 *
571 * @param string $meta_key The full meta key (including prefix).
572 * @param mixed $meta_value The value to check.
573 *
574 * @return bool
575 */
576 public static function meta_value_is_default( $meta_key, $meta_value ) {
577 return ( isset( self::$defaults[ $meta_key ] ) && $meta_value === self::$defaults[ $meta_key ] );
578 }
579
580 /**
581 * Get a custom post meta value.
582 *
583 * Returns the default value if the meta value has not been set.
584 *
585 * {@internal Unfortunately there isn't a filter available to hook into before returning
586 * the results for get_post_meta(), get_post_custom() and the likes. That
587 * would have been the preferred solution.}}
588 *
589 * @param string $key Internal key of the value to get (without prefix).
590 * @param int $postid Post ID of the post to get the value for.
591 *
592 * @return string All 'normal' values returned from get_post_meta() are strings.
593 * Objects and arrays are possible, but not used by this plugin
594 * and therefore discarted (except when the special 'serialized' field def
595 * value is set to true - only used by add-on plugins for now).
596 * Will return the default value if no value was found.
597 * Will return empty string if no default was found (not one of our keys) or
598 * if the post does not exist.
599 */
600 public static function get_value( $key, $postid = 0 ) {
601 global $post;
602
603 $postid = absint( $postid );
604 if ( $postid === 0 ) {
605 if ( ( isset( $post ) && is_object( $post ) ) && ( isset( $post->post_status ) && $post->post_status !== 'auto-draft' ) ) {
606 $postid = $post->ID;
607 }
608 else {
609 return '';
610 }
611 }
612
613 $custom = get_post_custom( $postid ); // Array of strings or empty array.
614 $table_key = self::$meta_prefix . $key;
615
616 // Populate the field_def using the field_index lookup array.
617 $field_def = [];
618 if ( isset( self::$fields_index[ $table_key ] ) ) {
619 $field_def = self::$meta_fields[ self::$fields_index[ $table_key ]['subset'] ][ self::$fields_index[ $table_key ]['key'] ];
620 }
621
622 // Check if we have a custom post meta entry.
623 if ( isset( $custom[ $table_key ][0] ) ) {
624 $unserialized = maybe_unserialize( $custom[ $table_key ][0] );
625
626 // Check if it is already unserialized.
627 if ( $custom[ $table_key ][0] === $unserialized ) {
628 return $custom[ $table_key ][0];
629 }
630
631 // Check whether we need to unserialize it.
632 if ( isset( $field_def['serialized'] ) && $field_def['serialized'] === true ) {
633 // Ok, serialize value expected/allowed.
634 return $unserialized;
635 }
636 }
637
638 // Meta was either not found or found, but object/array while not allowed to be.
639 if ( isset( self::$defaults[ self::$meta_prefix . $key ] ) ) {
640 // Update the default value to the current post type.
641 switch ( $key ) {
642 case 'schema_page_type':
643 case 'schema_article_type':
644 return '';
645 }
646
647 return self::$defaults[ self::$meta_prefix . $key ];
648 }
649
650 /*
651 * Shouldn't ever happen, means not one of our keys as there will always be a default available
652 * for all our keys.
653 */
654 return '';
655 }
656
657 /**
658 * Update a meta value for a post.
659 *
660 * @param string $key The internal key of the meta value to change (without prefix).
661 * @param mixed $meta_value The value to set the meta to.
662 * @param int $post_id The ID of the post to change the meta for.
663 *
664 * @return bool Whether the value was changed.
665 */
666 public static function set_value( $key, $meta_value, $post_id ) {
667 /*
668 * Slash the data, because `update_metadata` will unslash it and we have already unslashed it.
669 * Related issue: https://github.com/Yoast/YoastSEO.js/issues/2158
670 */
671 $meta_value = wp_slash( $meta_value );
672
673 return update_post_meta( $post_id, self::$meta_prefix . $key, $meta_value );
674 }
675
676 /**
677 * Deletes a meta value for a post.
678 *
679 * @param string $key The internal key of the meta value to change (without prefix).
680 * @param int $post_id The ID of the post to delete the meta for.
681 *
682 * @return bool Whether the delete was successful or not.
683 */
684 public static function delete( $key, $post_id ) {
685 return delete_post_meta( $post_id, self::$meta_prefix . $key );
686 }
687
688 /**
689 * Used for imports, this functions imports the value of $old_metakey into $new_metakey for those post
690 * where no WPSEO meta data has been set.
691 * Optionally deletes the $old_metakey values.
692 *
693 * @param string $old_metakey The old key of the meta value.
694 * @param string $new_metakey The new key, usually the WPSEO meta key (including prefix).
695 * @param bool $delete_old Whether to delete the old meta key/value-sets.
696 *
697 * @return void
698 */
699 public static function replace_meta( $old_metakey, $new_metakey, $delete_old = false ) {
700 global $wpdb;
701
702 /*
703 * Get only those rows where no wpseo meta values exist for the same post
704 * (with the exception of linkdex as that will be set independently of whether the post has been edited).
705 *
706 * {@internal Query is pretty well optimized this way.}}
707 */
708 $query = $wpdb->prepare(
709 "
710 SELECT `a`.*
711 FROM {$wpdb->postmeta} AS a
712 WHERE `a`.`meta_key` = %s
713 AND NOT EXISTS (
714 SELECT DISTINCT `post_id` , count( `meta_id` ) AS count
715 FROM {$wpdb->postmeta} AS b
716 WHERE `a`.`post_id` = `b`.`post_id`
717 AND `meta_key` LIKE %s
718 AND `meta_key` <> %s
719 GROUP BY `post_id`
720 )
721 ;",
722 $old_metakey,
723 $wpdb->esc_like( self::$meta_prefix . '%' ),
724 self::$meta_prefix . 'linkdex',
725 );
726 $oldies = $wpdb->get_results( $query );
727
728 if ( is_array( $oldies ) && $oldies !== [] ) {
729 foreach ( $oldies as $old ) {
730 update_post_meta( $old->post_id, $new_metakey, $old->meta_value );
731 }
732 }
733
734 // Delete old keys.
735 if ( $delete_old === true ) {
736 delete_post_meta_by_key( $old_metakey );
737 }
738 }
739
740 /**
741 * General clean-up of the saved meta values.
742 * - Remove potentially lingering old meta keys;
743 * - Remove all default and invalid values.
744 *
745 * @return void
746 */
747 public static function clean_up() {
748 global $wpdb;
749
750 /*
751 * Clean up '_yoast_wpseo_meta-robots'.
752 *
753 * Retrieve all '_yoast_wpseo_meta-robots' meta values and convert if no new values found.
754 *
755 * {@internal Query is pretty well optimized this way.}}
756 *
757 * @todo [JRF => Yoast] Find out all possible values which the old '_yoast_wpseo_meta-robots' could contain
758 * to convert the data correctly.
759 */
760 $query = $wpdb->prepare(
761 "
762 SELECT `a`.*
763 FROM {$wpdb->postmeta} AS a
764 WHERE `a`.`meta_key` = %s
765 AND NOT EXISTS (
766 SELECT DISTINCT `post_id` , count( `meta_id` ) AS count
767 FROM {$wpdb->postmeta} AS b
768 WHERE `a`.`post_id` = `b`.`post_id`
769 AND ( `meta_key` = %s
770 OR `meta_key` = %s )
771 GROUP BY `post_id`
772 )
773 ;",
774 self::$meta_prefix . 'meta-robots',
775 self::$meta_prefix . 'meta-robots-noindex',
776 self::$meta_prefix . 'meta-robots-nofollow',
777 );
778 $oldies = $wpdb->get_results( $query );
779
780 if ( is_array( $oldies ) && $oldies !== [] ) {
781 foreach ( $oldies as $old ) {
782 $old_values = explode( ',', $old->meta_value );
783 foreach ( $old_values as $value ) {
784 if ( $value === 'noindex' ) {
785 update_post_meta( $old->post_id, self::$meta_prefix . 'meta-robots-noindex', 1 );
786 }
787 elseif ( $value === 'nofollow' ) {
788 update_post_meta( $old->post_id, self::$meta_prefix . 'meta-robots-nofollow', 1 );
789 }
790 }
791 }
792 }
793 unset( $query, $oldies, $old, $old_values, $value );
794
795 // Delete old keys.
796 delete_post_meta_by_key( self::$meta_prefix . 'meta-robots' );
797
798 /*
799 * Remove all default values and (most) invalid option values.
800 * Invalid option values for the multiselect (meta-robots-adv) field will be dealt with seperately.
801 *
802 * {@internal Some of the defaults have changed in v1.5, but as the defaults will
803 * be removed and new defaults will now automatically be passed when no
804 * data found, this update is automatic (as long as we remove the old
805 * values which we do in the below routine).}}
806 *
807 * {@internal Unfortunately we can't use the normal delete_meta() with key/value combination
808 * as '' (empty string) values will be ignored and would result in all metas
809 * with that key being deleted, not just the empty fields.
810 * Still, the below implementation is largely based on the delete_meta() function.}}
811 */
812 $query = [];
813
814 foreach ( self::$meta_fields as $subset => $field_group ) {
815 foreach ( $field_group as $key => $field_def ) {
816 if ( ! isset( $field_def['default_value'] ) ) {
817 continue;
818 }
819
820 if ( isset( $field_def['options'] ) && is_array( $field_def['options'] ) && $field_def['options'] !== [] ) {
821 $valid = $field_def['options'];
822 // Remove the default value from the valid options.
823 unset( $valid[ $field_def['default_value'] ] );
824 $valid = array_keys( $valid );
825
826 $query[] = $wpdb->prepare(
827 "( meta_key = %s AND meta_value NOT IN ( '" . implode( "','", esc_sql( $valid ) ) . "' ) )",
828 self::$meta_prefix . $key,
829 );
830 unset( $valid );
831 }
832 elseif ( is_string( $field_def['default_value'] ) && $field_def['default_value'] !== '' ) {
833 $query[] = $wpdb->prepare(
834 '( meta_key = %s AND meta_value = %s )',
835 self::$meta_prefix . $key,
836 $field_def['default_value'],
837 );
838 }
839 else {
840 $query[] = $wpdb->prepare(
841 "( meta_key = %s AND meta_value = '' )",
842 self::$meta_prefix . $key,
843 );
844 }
845 }
846 }
847 unset( $subset, $field_group, $key, $field_def );
848
849 $query = "SELECT meta_id FROM {$wpdb->postmeta} WHERE " . implode( ' OR ', $query ) . ';';
850 $meta_ids = $wpdb->get_col( $query );
851
852 if ( is_array( $meta_ids ) && $meta_ids !== [] ) {
853 // WP native action.
854 do_action( 'delete_post_meta', $meta_ids, null, null, null );
855
856 $query = "DELETE FROM {$wpdb->postmeta} WHERE meta_id IN( " . implode( ',', $meta_ids ) . ' )';
857 $count = $wpdb->query( $query );
858
859 if ( $count ) {
860 foreach ( $meta_ids as $object_id ) {
861 wp_cache_delete( $object_id, 'post_meta' );
862 }
863
864 // WP native action.
865 do_action( 'deleted_post_meta', $meta_ids, null, null, null );
866 }
867 }
868 unset( $query, $meta_ids, $count, $object_id );
869
870 /*
871 * Deal with the multiselect (meta-robots-adv) field.
872 *
873 * Removes invalid option combinations, such as 'none,noarchive'.
874 *
875 * Default values have already been removed, so we should have a small result set and
876 * (hopefully) even smaller set of invalid results.
877 */
878 $query = $wpdb->prepare(
879 "SELECT meta_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s",
880 self::$meta_prefix . 'meta-robots-adv',
881 );
882 $oldies = $wpdb->get_results( $query );
883
884 if ( is_array( $oldies ) && $oldies !== [] ) {
885 foreach ( $oldies as $old ) {
886 $clean = self::validate_meta_robots_adv( $old->meta_value );
887
888 if ( $clean !== $old->meta_value ) {
889 if ( $clean !== self::$meta_fields['advanced']['meta-robots-adv']['default_value'] ) {
890 update_metadata_by_mid( 'post', $old->meta_id, $clean );
891 }
892 else {
893 delete_metadata_by_mid( 'post', $old->meta_id );
894 }
895 }
896 }
897 }
898 unset( $query, $oldies, $old, $clean );
899
900 do_action( 'wpseo_meta_clean_up' );
901 }
902
903 /**
904 * Recursively merge a variable number of arrays, using the left array as base,
905 * giving priority to the right array.
906 *
907 * Difference with native array_merge_recursive():
908 * array_merge_recursive converts values with duplicate keys to arrays rather than
909 * overwriting the value in the first array with the duplicate value in the second array.
910 *
911 * array_merge_recursive_distinct does not change the data types of the values in the arrays.
912 * Matching keys' values in the second array overwrite those in the first array, as is the
913 * case with array_merge.
914 *
915 * Freely based on information found on http://www.php.net/manual/en/function.array-merge-recursive.php
916 *
917 * {@internal Should be moved to a general utility class.}}
918 *
919 * @return array
920 */
921 public static function array_merge_recursive_distinct() {
922
923 $arrays = func_get_args();
924 if ( count( $arrays ) < 2 ) {
925 if ( $arrays === [] ) {
926 return [];
927 }
928 else {
929 return $arrays[0];
930 }
931 }
932
933 $merged = array_shift( $arrays );
934
935 foreach ( $arrays as $array ) {
936 foreach ( $array as $key => $value ) {
937 if ( is_array( $value ) && ( isset( $merged[ $key ] ) && is_array( $merged[ $key ] ) ) ) {
938 $merged[ $key ] = self::array_merge_recursive_distinct( $merged[ $key ], $value );
939 }
940 else {
941 $merged[ $key ] = $value;
942 }
943 }
944 unset( $key, $value );
945 }
946
947 return $merged;
948 }
949
950 /**
951 * Counts the total of all the keywords being used for posts except the given one.
952 *
953 * @param string $keyword The keyword to be counted.
954 * @param int $post_id The id of the post to which the keyword belongs.
955 *
956 * @return array
957 */
958 public static function keyword_usage( $keyword, $post_id ) {
959
960 if ( empty( $keyword ) ) {
961 return [];
962 }
963
964 /**
965 * The indexable repository.
966 *
967 * @var Indexable_Repository $repository
968 */
969 $repository = YoastSEO()->classes->get( Indexable_Repository::class );
970
971 $post_ids = $repository->query()
972 ->select( 'object_id' )
973 ->where( 'primary_focus_keyword', $keyword )
974 ->where( 'object_type', 'post' )
975 ->where_not_equal( 'object_id', $post_id )
976 ->where_not_equal( 'post_status', 'trash' )
977 ->limit( 2 ) // Limit to 2 results to save time and resources.
978 ->find_array();
979
980 // Get object_id from each subarray in $post_ids.
981 $post_ids = ( is_array( $post_ids ) ) ? array_column( $post_ids, 'object_id' ) : [];
982
983 /*
984 * If Premium is installed, get the additional keywords as well.
985 * We only check for the additional keywords if we've not already found two.
986 * In that case there's no use for an additional query as we already know
987 * that the keyword has been used multiple times before.
988 */
989 if ( count( $post_ids ) < 2 ) {
990 /**
991 * Allows enhancing the array of posts' that share their focus keywords with the post's focus keywords.
992 *
993 * @param array $post_ids The array of posts' ids that share their related keywords with the post.
994 * @param string $keyword The keyword to search for.
995 * @param int $post_id The id of the post the keyword is associated to.
996 */
997 $post_ids = apply_filters( 'wpseo_posts_for_focus_keyword', $post_ids, $keyword, $post_id );
998 }
999
1000 return $post_ids;
1001 }
1002
1003 /**
1004 * Returns the post types for the given post ids.
1005 *
1006 * @param array $post_ids The post ids to get the post types for.
1007 *
1008 * @return array The post types.
1009 */
1010 public static function post_types_for_ids( $post_ids ) {
1011 // Check if post ids is not empty.
1012 if ( ! empty( $post_ids ) ) {
1013 /**
1014 * The indexable repository.
1015 *
1016 * @var Indexable_Repository $repository
1017 */
1018 $repository = YoastSEO()->classes->get( Indexable_Repository::class );
1019
1020 // Get the post subtypes for the posts that share the keyword.
1021 $post_types = $repository->query()
1022 ->select( 'object_sub_type' )
1023 ->where_in( 'object_id', $post_ids )
1024 ->find_array();
1025
1026 // Get object_sub_type from each subarray in $post_ids.
1027 $post_types = array_column( $post_types, 'object_sub_type' );
1028 }
1029 else {
1030 $post_types = [];
1031 }
1032
1033 return $post_types;
1034 }
1035
1036 /**
1037 * Filter the schema article types.
1038 *
1039 * @return void
1040 */
1041 public static function filter_schema_article_types() {
1042 /** This filter is documented in inc/options/class-wpseo-option-titles.php */
1043 self::$meta_fields['schema']['schema_article_type']['options'] = apply_filters( 'wpseo_schema_article_types', self::$meta_fields['schema']['schema_article_type']['options'] );
1044 }
1045 }
1046