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-taxonomy-meta.php

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

569 lines 17.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WPSEO plugin file.
4 *
5 * @package WPSEO\Internals\Options
6 */
7
8 /**
9 * Option: wpseo_taxonomy_meta.
10 */
11 class WPSEO_Taxonomy_Meta extends WPSEO_Option {
12
13 /**
14 * Option name.
15 *
16 * @var string
17 */
18 public $option_name = 'wpseo_taxonomy_meta';
19
20 /**
21 * Whether to include the option in the return for WPSEO_Options::get_all().
22 *
23 * @var bool
24 */
25 public $include_in_all = false;
26
27 /**
28 * Array of defaults for the option.
29 *
30 * Shouldn't be requested directly, use $this->get_defaults();
31 *
32 * {@internal Important: in contrast to most defaults, the below array format is
33 * very bare. The real option is in the format [taxonomy_name][term_id][...]
34 * where [...] is any of the $defaults_per_term options shown below.
35 * This is of course taken into account in the below methods.}}
36 *
37 * @var array
38 */
39 protected $defaults = [];
40
41 /**
42 * Option name - same as $option_name property, but now also available to static methods.
43 *
44 * @var string
45 */
46 public static $name;
47
48 /**
49 * Array of defaults for individual taxonomy meta entries.
50 *
51 * @var array
52 */
53 public static $defaults_per_term = [
54 'wpseo_title' => '',
55 'wpseo_desc' => '',
56 'wpseo_canonical' => '',
57 'wpseo_bctitle' => '',
58 'wpseo_noindex' => 'default',
59 'wpseo_focuskw' => '',
60 'wpseo_linkdex' => '',
61 'wpseo_content_score' => '',
62 'wpseo_inclusive_language_score' => '',
63 'wpseo_focuskeywords' => '[]',
64 'wpseo_keywordsynonyms' => '[]',
65 'wpseo_is_cornerstone' => '0',
66
67 // Social fields.
68 'wpseo_opengraph-title' => '',
69 'wpseo_opengraph-description' => '',
70 'wpseo_opengraph-image' => '',
71 'wpseo_opengraph-image-id' => '',
72 'wpseo_twitter-title' => '',
73 'wpseo_twitter-description' => '',
74 'wpseo_twitter-image' => '',
75 'wpseo_twitter-image-id' => '',
76 ];
77
78 /**
79 * Available index options.
80 *
81 * Used for form generation and input validation.
82 *
83 * {@internal Labels (translation) added on admin_init via WPSEO_Taxonomy::translate_meta_options().}}
84 *
85 * @var array
86 */
87 public static $no_index_options = [
88 'default' => '',
89 'index' => '',
90 'noindex' => '',
91 ];
92
93 /**
94 * Add the actions and filters for the option.
95 *
96 * @todo [JRF => testers] Check if the extra actions below would run into problems if an option
97 * is updated early on and if so, change the call to schedule these for a later action on add/update
98 * instead of running them straight away.
99 */
100 protected function __construct() {
101 parent::__construct();
102
103 self::$name = $this->option_name;
104 }
105
106 /**
107 * Get the singleton instance of this class.
108 *
109 * @return object
110 */
111 public static function get_instance() {
112 if ( ! ( self::$instance instanceof self ) ) {
113 self::$instance = new self();
114 self::$name = self::$instance->option_name;
115 }
116
117 return self::$instance;
118 }
119
120 /**
121 * Add extra default options received from a filter.
122 *
123 * @return void
124 */
125 public function enrich_defaults() {
126 $extra_defaults_per_term = apply_filters( 'wpseo_add_extra_taxmeta_term_defaults', [] );
127 if ( is_array( $extra_defaults_per_term ) ) {
128 self::$defaults_per_term = array_merge( $extra_defaults_per_term, self::$defaults_per_term );
129 }
130 }
131
132 /**
133 * Validate the option.
134 *
135 * @param array $dirty New value for the option.
136 * @param array $clean Clean value for the option, normally the defaults.
137 * @param array $old Old value of the option.
138 *
139 * @return array Validated clean value for the option to be saved to the database.
140 */
141 protected function validate_option( $dirty, $clean, $old ) {
142 /*
143 * Prevent complete validation (which can be expensive when there are lots of terms)
144 * if only one item has changed and has already been validated.
145 */
146 if ( isset( $dirty['wpseo_already_validated'] ) && $dirty['wpseo_already_validated'] === true ) {
147 unset( $dirty['wpseo_already_validated'] );
148
149 return $dirty;
150 }
151
152 foreach ( $dirty as $taxonomy => $terms ) {
153 /* Don't validate taxonomy - may not be registered yet and we don't want to remove valid ones. */
154 if ( is_array( $terms ) && $terms !== [] ) {
155 foreach ( $terms as $term_id => $meta_data ) {
156 /* Only validate term if the taxonomy exists. */
157 if ( taxonomy_exists( $taxonomy ) && get_term_by( 'id', $term_id, $taxonomy ) === false ) {
158 /* Is this term id a special case ? */
159 if ( has_filter( 'wpseo_tax_meta_special_term_id_validation_' . $term_id ) !== false ) {
160 $clean[ $taxonomy ][ $term_id ] = apply_filters( 'wpseo_tax_meta_special_term_id_validation_' . $term_id, $meta_data, $taxonomy, $term_id );
161 }
162 continue;
163 }
164
165 if ( is_array( $meta_data ) && $meta_data !== [] ) {
166 /* Validate meta data. */
167 $old_meta = self::get_term_meta( $term_id, $taxonomy );
168 $meta_data = self::validate_term_meta_data( $meta_data, $old_meta );
169 if ( $meta_data !== [] ) {
170 $clean[ $taxonomy ][ $term_id ] = $meta_data;
171 }
172 }
173
174 // Deal with special cases (for when taxonomy doesn't exist yet).
175 if ( ! isset( $clean[ $taxonomy ][ $term_id ] ) && has_filter( 'wpseo_tax_meta_special_term_id_validation_' . $term_id ) !== false ) {
176 $clean[ $taxonomy ][ $term_id ] = apply_filters( 'wpseo_tax_meta_special_term_id_validation_' . $term_id, $meta_data, $taxonomy, $term_id );
177 }
178 }
179 }
180 }
181
182 return $clean;
183 }
184
185 /**
186 * Validate the meta data for one individual term and removes default values (no need to save those).
187 *
188 * @param array $meta_data New values.
189 * @param array $old_meta The original values.
190 *
191 * @return array Validated and filtered value.
192 */
193 public static function validate_term_meta_data( $meta_data, $old_meta ) {
194
195 $clean = self::$defaults_per_term;
196 $meta_data = array_map( [ 'WPSEO_Utils', 'trim_recursive' ], $meta_data );
197
198 if ( ! is_array( $meta_data ) || $meta_data === [] ) {
199 return $clean;
200 }
201
202 foreach ( $clean as $key => $value ) {
203 switch ( $key ) {
204
205 case 'wpseo_noindex':
206 if ( isset( $meta_data[ $key ] ) ) {
207 if ( isset( self::$no_index_options[ $meta_data[ $key ] ] ) ) {
208 $clean[ $key ] = $meta_data[ $key ];
209 }
210 }
211 elseif ( isset( $old_meta[ $key ] ) ) {
212 // Retain old value if field currently not in use.
213 $clean[ $key ] = $old_meta[ $key ];
214 }
215 break;
216
217 case 'wpseo_canonical':
218 if ( isset( $meta_data[ $key ] ) && $meta_data[ $key ] !== '' ) {
219 $url = WPSEO_Utils::sanitize_url( $meta_data[ $key ] );
220 if ( $url !== '' ) {
221 $clean[ $key ] = $url;
222 }
223 unset( $url );
224 }
225 break;
226
227 case 'wpseo_bctitle':
228 if ( isset( $meta_data[ $key ] ) ) {
229 $clean[ $key ] = WPSEO_Utils::sanitize_text_field( $meta_data[ $key ] );
230 }
231 elseif ( isset( $old_meta[ $key ] ) ) {
232 // Retain old value if field currently not in use.
233 $clean[ $key ] = $old_meta[ $key ];
234 }
235 break;
236
237 case 'wpseo_keywordsynonyms':
238 if ( isset( $meta_data[ $key ] ) && is_string( $meta_data[ $key ] ) ) {
239 // The data is stringified JSON. Use `json_decode` and `json_encode` around the sanitation.
240 $input = json_decode( $meta_data[ $key ], true );
241 // If something is wrong with the JSON make sure this cannot break.
242 $input ??= [];
243 $sanitized = array_map( [ 'WPSEO_Utils', 'sanitize_text_field' ], $input );
244 $clean[ $key ] = WPSEO_Utils::format_json_encode( $sanitized );
245 }
246 elseif ( isset( $old_meta[ $key ] ) ) {
247 // Retain old value if field currently not in use.
248 $clean[ $key ] = $old_meta[ $key ];
249 }
250 break;
251
252 case 'wpseo_focuskeywords':
253 if ( isset( $meta_data[ $key ] ) && is_string( $meta_data[ $key ] ) ) {
254 // The data is stringified JSON. Use `json_decode` and `json_encode` around the sanitation.
255 $input = json_decode( $meta_data[ $key ], true );
256
257 // This data has two known keys: `keyword` and `score`.
258 $sanitized = [];
259 foreach ( $input as $entry ) {
260 $sanitized[] = [
261 'keyword' => WPSEO_Utils::sanitize_text_field( $entry['keyword'] ),
262 'score' => WPSEO_Utils::sanitize_text_field( $entry['score'] ),
263 ];
264 }
265
266 $clean[ $key ] = WPSEO_Utils::format_json_encode( $sanitized );
267 }
268 elseif ( isset( $old_meta[ $key ] ) ) {
269 // Retain old value if field currently not in use.
270 $clean[ $key ] = $old_meta[ $key ];
271 }
272 break;
273
274 case 'wpseo_focuskw':
275 case 'wpseo_title':
276 case 'wpseo_desc':
277 case 'wpseo_linkdex':
278 default:
279 if ( isset( $meta_data[ $key ] ) && is_string( $meta_data[ $key ] ) ) {
280 $clean[ $key ] = WPSEO_Utils::sanitize_text_field( $meta_data[ $key ] );
281 }
282
283 if ( $key === 'wpseo_focuskw' ) {
284 $search = [
285 '&lt;',
286 '&gt;',
287 '&#96',
288 '<',
289 '>',
290 '`',
291 ];
292
293 $clean[ $key ] = str_replace( $search, '', $clean[ $key ] );
294 }
295 break;
296 }
297
298 $clean[ $key ] = apply_filters( 'wpseo_sanitize_tax_meta_' . $key, $clean[ $key ], ( $meta_data[ $key ] ?? null ), ( $old_meta[ $key ] ?? null ) );
299 }
300
301 // Only save the non-default values.
302 return array_diff_assoc( $clean, self::$defaults_per_term );
303 }
304
305 /**
306 * Clean a given option value.
307 * - Convert old option values to new
308 * - Fixes strings which were escaped (should have been sanitized - escaping is for output)
309 *
310 * @param array $option_value Old (not merged with defaults or filtered) option value to
311 * clean according to the rules for this option.
312 * @param string|null $current_version Optional. Version from which to upgrade, if not set,
313 * version specific upgrades will be disregarded.
314 * @param array|null $all_old_option_values Optional. Only used when importing old options to have
315 * access to the real old values, in contrast to the saved ones.
316 *
317 * @return array Cleaned option.
318 */
319 protected function clean_option( $option_value, $current_version = null, $all_old_option_values = null ) {
320
321 /* Clean up old values and remove empty arrays. */
322 if ( is_array( $option_value ) && $option_value !== [] ) {
323
324 foreach ( $option_value as $taxonomy => $terms ) {
325
326 if ( is_array( $terms ) && $terms !== [] ) {
327
328 foreach ( $terms as $term_id => $meta_data ) {
329 if ( ! is_array( $meta_data ) || $meta_data === [] ) {
330 // Remove empty term arrays.
331 unset( $option_value[ $taxonomy ][ $term_id ] );
332 }
333 else {
334 foreach ( $meta_data as $key => $value ) {
335
336 switch ( $key ) {
337 case 'noindex':
338 if ( $value === 'on' ) {
339 // Convert 'on' to 'noindex'.
340 $option_value[ $taxonomy ][ $term_id ][ $key ] = 'noindex';
341 }
342 break;
343
344 case 'canonical':
345 case 'wpseo_bctitle':
346 case 'wpseo_title':
347 case 'wpseo_desc':
348 case 'wpseo_linkdex':
349 // @todo [JRF => whomever] Needs checking, I don't have example data [JRF].
350 if ( $value !== '' ) {
351 // Fix incorrectly saved (encoded) canonical urls and texts.
352 $option_value[ $taxonomy ][ $term_id ][ $key ] = wp_specialchars_decode( stripslashes( $value ), ENT_QUOTES );
353 }
354 break;
355
356 default:
357 // @todo [JRF => whomever] Needs checking, I don't have example data [JRF].
358 if ( $value !== '' ) {
359 // Fix incorrectly saved (escaped) text strings.
360 $option_value[ $taxonomy ][ $term_id ][ $key ] = wp_specialchars_decode( $value, ENT_QUOTES );
361 }
362 break;
363 }
364 }
365 }
366 }
367 }
368 else {
369 // Remove empty taxonomy arrays.
370 unset( $option_value[ $taxonomy ] );
371 }
372 }
373 }
374
375 return $option_value;
376 }
377
378 /**
379 * Retrieve a taxonomy term's meta value(s).
380 *
381 * @param mixed $term Term to get the meta value for
382 * either (string) term name, (int) term id or (object) term.
383 * @param string $taxonomy Name of the taxonomy to which the term is attached.
384 * @param string|null $meta Optional. Meta value to get (without prefix).
385 *
386 * @return mixed Value for the $meta if one is given, might be the default.
387 * If no meta is given, an array of all the meta data for the term.
388 * False if the term does not exist or the $meta provided is invalid.
389 */
390 public static function get_term_meta( $term, $taxonomy, $meta = null ) {
391 /* Figure out the term id. */
392 if ( is_int( $term ) ) {
393 $term = get_term_by( 'id', $term, $taxonomy );
394 }
395 elseif ( is_string( $term ) ) {
396 $term = get_term_by( 'slug', $term, $taxonomy );
397 }
398
399 if ( is_object( $term ) && isset( $term->term_id ) ) {
400 $term_id = $term->term_id;
401 }
402 else {
403 return false;
404 }
405
406 $tax_meta = self::get_term_tax_meta( $term_id, $taxonomy );
407
408 /*
409 * Either return the complete array or a single value from it or false if the value does not exist
410 * (shouldn't happen after merge with defaults, indicates typo in request).
411 */
412 if ( ! isset( $meta ) ) {
413 return $tax_meta;
414 }
415
416 if ( isset( $tax_meta[ 'wpseo_' . $meta ] ) ) {
417 return $tax_meta[ 'wpseo_' . $meta ];
418 }
419
420 return false;
421 }
422
423 /**
424 * Get the current queried object and return the meta value.
425 *
426 * @param string $meta The meta field that is needed.
427 *
428 * @return mixed
429 */
430 public static function get_meta_without_term( $meta ) {
431 $term = $GLOBALS['wp_query']->get_queried_object();
432 if ( ! $term || empty( $term->taxonomy ) ) {
433 return false;
434 }
435
436 return self::get_term_meta( $term, $term->taxonomy, $meta );
437 }
438
439 /**
440 * Saving the values for the given term_id.
441 *
442 * @param int $term_id ID of the term to save data for.
443 * @param string $taxonomy The taxonomy the term belongs to.
444 * @param array $meta_values The values that will be saved.
445 *
446 * @return void
447 */
448 public static function set_values( $term_id, $taxonomy, array $meta_values ) {
449 /* Validate the post values */
450 $old = self::get_term_meta( $term_id, $taxonomy );
451 $clean = self::validate_term_meta_data( $meta_values, $old );
452
453 self::save_clean_values( $term_id, $taxonomy, $clean );
454 }
455
456 /**
457 * Setting a single value to the term meta.
458 *
459 * @param int $term_id ID of the term to save data for.
460 * @param string $taxonomy The taxonomy the term belongs to.
461 * @param string $meta_key The target meta key to store the value in.
462 * @param string $meta_value The value of the target meta key.
463 *
464 * @return void
465 */
466 public static function set_value( $term_id, $taxonomy, $meta_key, $meta_value ) {
467
468 if ( substr( strtolower( $meta_key ), 0, 6 ) !== 'wpseo_' ) {
469 $meta_key = 'wpseo_' . $meta_key;
470 }
471
472 self::set_values( $term_id, $taxonomy, [ $meta_key => $meta_value ] );
473 }
474
475 /**
476 * Find the keyword usages in the metas for the taxonomies/terms.
477 *
478 * @param string $keyword The keyword to look for.
479 * @param string $current_term_id The current term id.
480 * @param string $current_taxonomy The current taxonomy name.
481 *
482 * @return array
483 */
484 public static function get_keyword_usage( $keyword, $current_term_id, $current_taxonomy ) {
485 $tax_meta = self::get_tax_meta();
486
487 $found = [];
488 // @todo Check for terms of all taxonomies, not only the current taxonomy.
489 foreach ( $tax_meta as $taxonomy_name => $terms ) {
490 foreach ( $terms as $term_id => $meta_values ) {
491 $is_current = ( $current_taxonomy === $taxonomy_name && (string) $current_term_id === (string) $term_id );
492 if ( ! $is_current && ! empty( $meta_values['wpseo_focuskw'] ) && $meta_values['wpseo_focuskw'] === $keyword ) {
493 $found[] = $term_id;
494 }
495 }
496 }
497
498 return [ $keyword => $found ];
499 }
500
501 /**
502 * Saving the values for the given term_id.
503 *
504 * @param int $term_id ID of the term to save data for.
505 * @param string $taxonomy The taxonomy the term belongs to.
506 * @param array $clean Array with clean values.
507 *
508 * @return void
509 */
510 private static function save_clean_values( $term_id, $taxonomy, array $clean ) {
511 $tax_meta = self::get_tax_meta();
512
513 /* Add/remove the result to/from the original option value. */
514 if ( $clean !== [] ) {
515 $tax_meta[ $taxonomy ][ $term_id ] = $clean;
516 }
517 else {
518 unset( $tax_meta[ $taxonomy ][ $term_id ] );
519 if ( isset( $tax_meta[ $taxonomy ] ) && $tax_meta[ $taxonomy ] === [] ) {
520 unset( $tax_meta[ $taxonomy ] );
521 }
522 }
523
524 // Prevent complete array validation.
525 $tax_meta['wpseo_already_validated'] = true;
526
527 self::save_tax_meta( $tax_meta );
528 }
529
530 /**
531 * Getting the meta from the options.
532 *
533 * @return array|void
534 */
535 private static function get_tax_meta() {
536 return get_option( self::$name );
537 }
538
539 /**
540 * Saving the tax meta values to the database.
541 *
542 * @param array $tax_meta Array with the meta values for taxonomy.
543 *
544 * @return void
545 */
546 private static function save_tax_meta( $tax_meta ) {
547 update_option( self::$name, $tax_meta );
548 }
549
550 /**
551 * Getting the taxonomy meta for the given term_id and taxonomy.
552 *
553 * @param int $term_id The id of the term.
554 * @param string $taxonomy Name of the taxonomy to which the term is attached.
555 *
556 * @return array
557 */
558 private static function get_term_tax_meta( $term_id, $taxonomy ) {
559 $tax_meta = self::get_tax_meta();
560
561 /* If we have data for the term, merge with defaults for complete array, otherwise set defaults. */
562 if ( isset( $tax_meta[ $taxonomy ][ $term_id ] ) ) {
563 return array_merge( self::$defaults_per_term, $tax_meta[ $taxonomy ][ $term_id ] );
564 }
565
566 return self::$defaults_per_term;
567 }
568 }
569