PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.9
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.9
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 / class-wpseo-replace-vars.php

class-wpseo-replace-vars.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 27.9, at inc/class-wpseo-replace-vars.php

1,647 lines 51.5 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
6 * @since 1.5.4
7 */
8
9 // Avoid direct calls to this file.
10 if ( ! defined( 'WPSEO_VERSION' ) ) {
11 header( 'Status: 403 Forbidden' );
12 header( 'HTTP/1.1 403 Forbidden' );
13 exit();
14 }
15
16 /**
17 * Class: WPSEO_Replace_Vars.
18 *
19 * This class implements the replacing of `%%variable_placeholders%%` with their real value based on the current
20 * requested page/post/cpt/etc in text strings.
21 */
22 class WPSEO_Replace_Vars {
23
24 /**
25 * Default post/page/cpt information.
26 *
27 * @var array
28 */
29 protected $defaults = [
30 'ID' => '',
31 'name' => '',
32 'post_author' => '',
33 'post_content' => '',
34 'post_date' => '',
35 'post_excerpt' => '',
36 'post_modified' => '',
37 'post_title' => '',
38 'taxonomy' => '',
39 'term_id' => '',
40 'term404' => '',
41 ];
42
43 /**
44 * Current post/page/cpt information.
45 *
46 * @var stdClass
47 */
48 protected $args;
49
50 /**
51 * Help texts for use in WPSEO -> Search appearance tabs.
52 *
53 * @var array
54 */
55 protected static $help_texts = [];
56
57 /**
58 * Register of additional variable replacements registered by other plugins/themes.
59 *
60 * @var array
61 */
62 protected static $external_replacements = [];
63
64 /**
65 * Setup the help texts and external replacements as statics so they will be available to all instances.
66 *
67 * @return void
68 */
69 public static function setup_statics_once() {
70 if ( self::$help_texts === [] ) {
71 self::set_basic_help_texts();
72 self::set_advanced_help_texts();
73 }
74
75 if ( self::$external_replacements === [] ) {
76 /**
77 * Action: 'wpseo_register_extra_replacements' - Allows for registration of additional
78 * variables to replace.
79 */
80 do_action( 'wpseo_register_extra_replacements' );
81 }
82 }
83
84 /**
85 * Register new replacement %%variables%%.
86 * For use by other plugins/themes to register extra variables.
87 *
88 * @see wpseo_register_var_replacement() for a usage example.
89 *
90 * @param string $var_to_replace The name of the variable to replace, i.e. '%%var%%'.
91 * Note: the surrounding %% are optional.
92 * @param mixed $replace_function Function or method to call to retrieve the replacement value for the variable.
93 * Uses the same format as add_filter/add_action function parameter and
94 * should *return* the replacement value. DON'T echo it.
95 * @param string $type Type of variable: 'basic' or 'advanced', defaults to 'advanced'.
96 * @param string $help_text Help text to be added to the help tab for this variable.
97 *
98 * @return bool Whether the replacement function was succesfully registered.
99 */
100 public static function register_replacement( $var_to_replace, $replace_function, $type = 'advanced', $help_text = '' ) {
101 $success = false;
102
103 if ( is_string( $var_to_replace ) && $var_to_replace !== '' ) {
104 $var_to_replace = self::remove_var_delimiter( $var_to_replace );
105
106 if ( preg_match( '`^[A-Z0-9_-]+$`i', $var_to_replace ) === false ) {
107 trigger_error( esc_html__( 'A replacement variable can only contain alphanumeric characters, an underscore or a dash. Try renaming your variable.', 'wordpress-seo' ), E_USER_WARNING );
108 }
109 elseif ( strpos( $var_to_replace, 'cf_' ) === 0 || strpos( $var_to_replace, 'ct_' ) === 0 ) {
110 trigger_error( esc_html__( 'A replacement variable can not start with "%%cf_" or "%%ct_" as these are reserved for the WPSEO standard variable variables for custom fields and custom taxonomies. Try making your variable name unique.', 'wordpress-seo' ), E_USER_WARNING );
111 }
112 elseif ( ! method_exists( self::class, 'retrieve_' . $var_to_replace ) ) {
113 if ( $var_to_replace !== '' && ! isset( self::$external_replacements[ $var_to_replace ] ) ) {
114 self::$external_replacements[ $var_to_replace ] = $replace_function;
115 $replacement_variable = new WPSEO_Replacement_Variable( $var_to_replace, $var_to_replace, $help_text );
116 self::register_help_text( $type, $replacement_variable );
117 $success = true;
118 }
119 else {
120 trigger_error( esc_html__( 'A replacement variable with the same name has already been registered. Try making your variable name unique.', 'wordpress-seo' ), E_USER_WARNING );
121 }
122 }
123 else {
124 trigger_error( esc_html__( 'You cannot overrule a WPSEO standard variable replacement by registering a variable with the same name. Use the "wpseo_replacements" filter instead to adjust the replacement value.', 'wordpress-seo' ), E_USER_WARNING );
125 }
126 }
127
128 return $success;
129 }
130
131 /**
132 * Replace `%%variable_placeholders%%` with their real value based on the current requested page/post/cpt/etc.
133 *
134 * @param string $text The string to replace the variables in.
135 * @param array $args The object some of the replacement values might come from,
136 * could be a post, taxonomy or term.
137 * @param array $omit Variables that should not be replaced by this function.
138 *
139 * @return string
140 */
141 public function replace( $text, $args, $omit = [] ) {
142
143 $text = wp_strip_all_tags( $text );
144
145 // Let's see if we can bail super early.
146 if ( strpos( $text, '%%' ) === false ) {
147 return YoastSEO()->helpers->string->standardize_whitespace( $text );
148 }
149
150 $args = (array) $args;
151 if ( isset( $args['post_content'] ) && ! empty( $args['post_content'] ) ) {
152 $args['post_content'] = YoastSEO()->helpers->string->strip_shortcode( $args['post_content'] );
153 }
154 if ( isset( $args['post_excerpt'] ) && ! empty( $args['post_excerpt'] ) ) {
155 $args['post_excerpt'] = YoastSEO()->helpers->string->strip_shortcode( $args['post_excerpt'] );
156 }
157 $this->args = (object) wp_parse_args( $args, $this->defaults );
158
159 // Clean $omit array.
160 if ( is_array( $omit ) && $omit !== [] ) {
161 $omit = array_map( [ self::class, 'remove_var_delimiter' ], $omit );
162 }
163
164 $replacements = [];
165 if ( preg_match_all( '`%%([^%]+(%%single)?)%%?`iu', $text, $matches ) ) {
166 $replacements = $this->set_up_replacements( $matches, $omit );
167 }
168
169 /**
170 * Filter: 'wpseo_replacements' - Allow customization of the replacements before they are applied.
171 *
172 * @param array $replacements The replacements.
173 * @param array $args The object some of the replacement values might come from,
174 * could be a post, taxonomy or term.
175 */
176 $replacements = apply_filters( 'wpseo_replacements', $replacements, $this->args );
177
178 // Do the actual replacements.
179 if ( is_array( $replacements ) && $replacements !== [] ) {
180 $text = str_replace(
181 array_keys( $replacements ),
182 // Make sure to exclude replacement values that are arrays e.g. coming from a custom field serialized value.
183 array_filter( array_values( $replacements ), 'is_scalar' ),
184 $text,
185 );
186 }
187
188 /**
189 * Filter: 'wpseo_replacements_final' - Allow overruling of whether or not to remove placeholders
190 * which didn't yield a replacement.
191 *
192 * @example <code>add_filter( 'wpseo_replacements_final', '__return_false' );</code>
193 *
194 * @param bool $final
195 */
196 if ( apply_filters( 'wpseo_replacements_final', true ) === true && ( isset( $matches[1] ) && is_array( $matches[1] ) ) ) {
197 // Remove non-replaced variables.
198 $remove = array_diff( $matches[1], $omit ); // Make sure the $omit variables do not get removed.
199 $remove = array_map( [ self::class, 'add_var_delimiter' ], $remove );
200 $text = str_replace( $remove, '', $text );
201 }
202
203 // Undouble separators which have nothing between them, i.e. where a non-replaced variable was removed.
204 if ( isset( $replacements['%%sep%%'] ) && ( is_string( $replacements['%%sep%%'] ) && $replacements['%%sep%%'] !== '' ) ) {
205 $q_sep = preg_quote( $replacements['%%sep%%'], '`' );
206 $text = preg_replace( '`' . $q_sep . '(?:\s*' . $q_sep . ')*`u', $replacements['%%sep%%'], $text );
207 }
208
209 // Remove superfluous whitespace.
210 $text = YoastSEO()->helpers->string->standardize_whitespace( $text );
211
212 return $text;
213 }
214
215 /**
216 * Register a new replacement variable if it has not been registered already.
217 *
218 * @param string $var_to_replace The name of the variable to replace, i.e. '%%var%%'.
219 * Note: the surrounding %% are optional.
220 * @param mixed $replace_function Function or method to call to retrieve the replacement value for the variable.
221 * Uses the same format as add_filter/add_action function parameter and
222 * should *return* the replacement value. DON'T echo it.
223 * @param string $type Type of variable: 'basic' or 'advanced', defaults to 'advanced'.
224 * @param string $help_text Help text to be added to the help tab for this variable.
225 *
226 * @return bool `true` if the replace var has been registered, `false` if not.
227 */
228 public function safe_register_replacement( $var_to_replace, $replace_function, $type = 'advanced', $help_text = '' ) {
229 if ( ! $this->has_been_registered( $var_to_replace ) ) {
230 return self::register_replacement( $var_to_replace, $replace_function, $type, $help_text );
231 }
232 return false;
233 }
234
235 /**
236 * Checks whether the given replacement variable has already been registered or not.
237 *
238 * @param string $replacement_variable The replacement variable to check, including the variable delimiter (e.g. `%%var%%`).
239 *
240 * @return bool `true` if the replacement variable has already been registered.
241 */
242 public function has_been_registered( $replacement_variable ) {
243 $replacement_variable = self::remove_var_delimiter( $replacement_variable );
244
245 return isset( self::$external_replacements[ $replacement_variable ] );
246 }
247
248 /**
249 * Returns the list of hidden replace vars.
250 *
251 * E.g. the replace vars that should work, but are not advertised.
252 *
253 * @return string[] The list of hidden replace vars.
254 */
255 public function get_hidden_replace_vars() {
256 return [
257 'currentdate',
258 'currentyear',
259 'currentmonth',
260 'currentday',
261 'post_year',
262 'post_month',
263 'post_day',
264 'author_first_name',
265 'author_last_name',
266 'permalink',
267 'post_content',
268 'category_title',
269 ];
270 }
271
272 /**
273 * Retrieve the replacements for the variables found.
274 *
275 * @param array $matches Variables found in the original string - regex result.
276 * @param array $omit Variables that should not be replaced by this function.
277 *
278 * @return array Retrieved replacements - this might be a smaller array as some variables
279 * may not yield a replacement in certain contexts.
280 */
281 private function set_up_replacements( $matches, $omit ) {
282
283 $replacements = [];
284
285 // @todo Figure out a way to deal with external functions starting with cf_/ct_.
286 foreach ( $matches[1] as $k => $var ) {
287
288 // Don't set up replacements which should be omitted.
289 if ( in_array( $var, $omit, true ) ) {
290 continue;
291 }
292
293 // Deal with variable variable names first.
294 if ( strpos( $var, 'cf_' ) === 0 ) {
295 $replacement = $this->retrieve_cf_custom_field_name( $var );
296 }
297 elseif ( strpos( $var, 'ct_desc_' ) === 0 ) {
298 $replacement = $this->retrieve_ct_desc_custom_tax_name( $var );
299 }
300 elseif ( strpos( $var, 'ct_' ) === 0 ) {
301 $single = ( isset( $matches[2][ $k ] ) && $matches[2][ $k ] !== '' );
302 $replacement = $this->retrieve_ct_custom_tax_name( $var, $single );
303 }
304 // Deal with non-variable variable names.
305 elseif ( method_exists( $this, 'retrieve_' . $var ) ) {
306 $method_name = 'retrieve_' . $var;
307 $replacement = $this->$method_name();
308 }
309 // Deal with externally defined variable names.
310 elseif ( isset( self::$external_replacements[ $var ] ) && is_callable( self::$external_replacements[ $var ] ) ) {
311 $replacement = call_user_func( self::$external_replacements[ $var ], $var, $this->args );
312 }
313
314 // Replacement retrievals can return null if no replacement can be determined, root those outs.
315 if ( isset( $replacement ) ) {
316 $var = self::add_var_delimiter( $var );
317 $replacements[ $var ] = $replacement;
318 }
319 unset( $replacement, $single, $method_name );
320 }
321
322 return $replacements;
323 }
324
325 /* *********************** BASIC VARIABLES ************************** */
326
327 /**
328 * Retrieve the post/cpt categories (comma separated) for use as replacement string.
329 *
330 * @return string|null
331 */
332 private function retrieve_category() {
333 $replacement = null;
334
335 if ( ! empty( $this->args->ID ) ) {
336 $cat = $this->get_terms( $this->args->ID, 'category' );
337 if ( $cat !== '' ) {
338 return $cat;
339 }
340 }
341
342 if ( isset( $this->args->cat_name ) && ! empty( $this->args->cat_name ) ) {
343 $replacement = $this->args->cat_name;
344 }
345
346 return $replacement;
347 }
348
349 /**
350 * Retrieve the category description for use as replacement string.
351 *
352 * @return string|null
353 */
354 private function retrieve_category_description() {
355 return $this->retrieve_term_description();
356 }
357
358 /**
359 * Retrieve the date of the post/page/cpt for use as replacement string.
360 *
361 * @return string|null
362 */
363 private function retrieve_date() {
364 $replacement = null;
365
366 if ( $this->args->post_date !== '' ) {
367 // Returns a string.
368 $replacement = YoastSEO()->helpers->date->format_translated( $this->args->post_date, get_option( 'date_format' ) );
369 }
370 elseif ( get_query_var( 'day' ) && get_query_var( 'day' ) !== '' ) {
371 // Returns a string.
372 $replacement = get_the_date();
373 }
374 elseif ( single_month_title( ' ', false ) ) {
375 // Returns a string.
376 $replacement = single_month_title( ' ', false );
377 }
378 elseif ( get_query_var( 'year' ) !== '' ) {
379 // Returns an integer, let's cast to string.
380 $replacement = (string) get_query_var( 'year' );
381 }
382
383 return $replacement;
384 }
385
386 /**
387 * Retrieve the post/page/cpt excerpt for use as replacement string.
388 * The excerpt will be auto-generated if it does not exist.
389 *
390 * @return string|null
391 */
392 private function retrieve_excerpt() {
393 $replacement = null;
394 $locale = get_locale();
395
396 // Japanese doesn't have a jp_JP variant in WP.
397 $limit = ( $locale === 'ja' ) ? 80 : 156;
398
399 // The check `post_password_required` is because excerpt must be hidden for a post with a password.
400 if ( ! empty( $this->args->ID ) && ! post_password_required( $this->args->ID ) ) {
401 if ( $this->args->post_excerpt !== '' ) {
402 $replacement = wp_strip_all_tags( $this->args->post_excerpt );
403 }
404 elseif ( $this->args->post_content !== '' ) {
405 $content = strip_shortcodes( $this->args->post_content );
406 $content = wp_strip_all_tags( $content );
407
408 if ( mb_strlen( $content ) <= $limit ) {
409 return $content;
410 }
411
412 $replacement = wp_html_excerpt( $content, $limit );
413
414 // Check if the description has space and trim the auto-generated string to a word boundary.
415 if ( strrpos( $replacement, ' ' ) ) {
416 $replacement = substr( $replacement, 0, strrpos( $replacement, ' ' ) );
417 }
418 }
419 }
420
421 return $replacement;
422 }
423
424 /**
425 * Retrieve the post/page/cpt excerpt for use as replacement string (without auto-generation).
426 *
427 * @return string|null
428 */
429 private function retrieve_excerpt_only() {
430 $replacement = null;
431
432 // The check `post_password_required` is because excerpt must be hidden for a post with a password.
433 if ( ! empty( $this->args->ID ) && $this->args->post_excerpt !== '' && ! post_password_required( $this->args->ID ) ) {
434 $replacement = wp_strip_all_tags( $this->args->post_excerpt );
435 }
436
437 return $replacement;
438 }
439
440 /**
441 * Retrieve the title of the parent page of the current page/cpt for use as replacement string.
442 * Only applicable for hierarchical post types.
443 *
444 * @todo Check: shouldn't this use $this->args as well ?
445 *
446 * @return string|null
447 */
448 private function retrieve_parent_title() {
449 $replacement = null;
450
451 if ( ! empty( $this->args->ID ) ) {
452 $parent_id = wp_get_post_parent_id( $this->args->ID );
453 if ( $parent_id ) {
454 $replacement = get_the_title( $parent_id );
455 }
456 }
457
458 return $replacement;
459 }
460
461 /**
462 * Retrieve the current search phrase for use as replacement string.
463 *
464 * @return string|null
465 */
466 private function retrieve_searchphrase() {
467 $replacement = null;
468
469 $search = get_query_var( 's' );
470 if ( $search !== '' ) {
471 $replacement = esc_html( $search );
472 }
473
474 return $replacement;
475 }
476
477 /**
478 * Retrieve the separator for use as replacement string.
479 *
480 * @return string Retrieves the title separator.
481 */
482 private function retrieve_sep() {
483 return YoastSEO()->helpers->options->get_title_separator();
484 }
485
486 /**
487 * Retrieve the site's tag line / description for use as replacement string.
488 *
489 * The `$replacement` variable is static because it doesn't change depending
490 * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482.
491 *
492 * @return string|null
493 */
494 private function retrieve_sitedesc() {
495 static $replacement;
496
497 if ( ! isset( $replacement ) ) {
498 $description = wp_strip_all_tags( get_bloginfo( 'description' ) );
499 if ( $description !== '' ) {
500 $replacement = $description;
501 }
502 }
503
504 return $replacement;
505 }
506
507 /**
508 * Retrieve the site's name for use as replacement string.
509 *
510 * The `$replacement` variable is static because it doesn't change depending
511 * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482.
512 *
513 * @return string|null
514 */
515 private function retrieve_sitename() {
516 static $replacement;
517
518 if ( ! isset( $replacement ) ) {
519 $sitename = YoastSEO()->helpers->site->get_site_name();
520 if ( $sitename !== '' ) {
521 $replacement = $sitename;
522 }
523 }
524
525 return $replacement;
526 }
527
528 /**
529 * Retrieve the current tag/tags for use as replacement string.
530 *
531 * @return string|null
532 */
533 private function retrieve_tag() {
534 $replacement = null;
535
536 if ( ! empty( $this->args->ID ) ) {
537 $tags = $this->get_terms( $this->args->ID, 'post_tag' );
538 if ( $tags !== '' ) {
539 $replacement = $tags;
540 }
541 }
542
543 return $replacement;
544 }
545
546 /**
547 * Retrieve the tag description for use as replacement string.
548 *
549 * @return string|null
550 */
551 private function retrieve_tag_description() {
552 return $this->retrieve_term_description();
553 }
554
555 /**
556 * Retrieve the term description for use as replacement string.
557 *
558 * @return string|null
559 */
560 private function retrieve_term_description() {
561 $replacement = null;
562
563 if ( ! empty( $this->args->term_id ) && ! empty( $this->args->taxonomy ) ) {
564 $term_desc = get_term_field( 'description', $this->args->term_id, $this->args->taxonomy );
565 if ( $term_desc !== '' ) {
566 $replacement = wp_strip_all_tags( $term_desc );
567 }
568 }
569
570 return $replacement;
571 }
572
573 /**
574 * Retrieve the term name for use as replacement string.
575 *
576 * @return string|null
577 */
578 private function retrieve_term_title() {
579 $replacement = null;
580
581 if ( ! empty( $this->args->taxonomy ) && ! empty( $this->args->name ) ) {
582 $replacement = $this->args->name;
583 }
584
585 return $replacement;
586 }
587
588 /**
589 * Retrieve the title of the post/page/cpt for use as replacement string.
590 *
591 * @return string|null
592 */
593 private function retrieve_title() {
594 $replacement = null;
595
596 if ( is_string( $this->args->post_title ) && $this->args->post_title !== '' ) {
597 $replacement = $this->args->post_title;
598 }
599
600 return $replacement;
601 }
602
603 /**
604 * Retrieve primary category for use as replacement string.
605 *
606 * @return bool|int|null
607 */
608 private function retrieve_primary_category() {
609 $primary_category = null;
610
611 if ( ! empty( $this->args->ID ) ) {
612 $wpseo_primary_category = new WPSEO_Primary_Term( 'category', $this->args->ID );
613
614 $term_id = $wpseo_primary_category->get_primary_term();
615 $term = get_term( $term_id );
616
617 if ( ! is_wp_error( $term ) && ! empty( $term ) ) {
618 $primary_category = $term->name;
619 }
620 }
621
622 return $primary_category;
623 }
624
625 /**
626 * Retrieve the string generated by get_the_archive_title().
627 *
628 * @return string|null
629 */
630 private function retrieve_archive_title() {
631 return get_the_archive_title();
632 }
633
634 /* *********************** ADVANCED VARIABLES ************************** */
635
636 /**
637 * Determine the page numbering of the current post/page/cpt.
638 *
639 * @param string $request Either 'nr'|'max' - whether to return the page number or the max number of pages.
640 *
641 * @return int|null
642 */
643 private function determine_pagenumbering( $request = 'nr' ) {
644 global $wp_query, $post;
645 $max_num_pages = null;
646 $page_number = null;
647
648 $max_num_pages = 1;
649
650 if ( ! is_singular() ) {
651 $page_number = get_query_var( 'paged' );
652 if ( $page_number === 0 || $page_number === '' ) {
653 $page_number = 1;
654 }
655
656 if ( ! empty( $wp_query->max_num_pages ) ) {
657 $max_num_pages = $wp_query->max_num_pages;
658 }
659 }
660 else {
661 $page_number = get_query_var( 'page' );
662 if ( $page_number === 0 || $page_number === '' ) {
663 $page_number = 1;
664 }
665
666 if ( isset( $post->post_content ) ) {
667 $max_num_pages = ( substr_count( $post->post_content, '<!--nextpage-->' ) + 1 );
668 }
669 }
670
671 $return = null;
672
673 switch ( $request ) {
674 case 'nr':
675 $return = $page_number;
676 break;
677 case 'max':
678 $return = $max_num_pages;
679 break;
680 }
681
682 return $return;
683 }
684
685 /**
686 * Determine the post type names for the current post/page/cpt.
687 *
688 * @param string $request Either 'single'|'plural' - whether to return the single or plural form.
689 *
690 * @return string|null
691 */
692 private function determine_pt_names( $request = 'single' ) {
693 global $wp_query;
694 $pt_single = null;
695 $pt_plural = null;
696 $post_type = '';
697
698 if ( isset( $wp_query->query_vars['post_type'] ) && ( ( is_string( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] !== '' ) || ( is_array( $wp_query->query_vars['post_type'] ) && $wp_query->query_vars['post_type'] !== [] ) ) ) {
699 $post_type = $wp_query->query_vars['post_type'];
700 }
701 elseif ( isset( $this->args->post_type ) && ( is_string( $this->args->post_type ) && $this->args->post_type !== '' ) ) {
702 $post_type = $this->args->post_type;
703 }
704 else {
705 // Make it work in preview mode.
706 $post = $wp_query->get_queried_object();
707 if ( $post instanceof WP_Post ) {
708 $post_type = $post->post_type;
709 }
710 }
711
712 if ( is_array( $post_type ) ) {
713 $post_type = reset( $post_type );
714 }
715
716 if ( $post_type !== '' ) {
717 $pt = get_post_type_object( $post_type );
718 $pt_single = $pt->name;
719 $pt_plural = $pt->name;
720 if ( isset( $pt->labels->singular_name ) ) {
721 $pt_single = $pt->labels->singular_name;
722 }
723 if ( isset( $pt->labels->name ) ) {
724 $pt_plural = $pt->labels->name;
725 }
726 }
727
728 $return = null;
729
730 switch ( $request ) {
731 case 'single':
732 $return = $pt_single;
733 break;
734 case 'plural':
735 $return = $pt_plural;
736 break;
737 }
738
739 return $return;
740 }
741
742 /**
743 * Retrieve the attachment caption for use as replacement string.
744 *
745 * @return string|null
746 */
747 private function retrieve_caption() {
748 return $this->retrieve_excerpt_only();
749 }
750
751 /**
752 * Retrieve a post/page/cpt's custom field value for use as replacement string.
753 *
754 * @param string $var_to_replace The complete variable to replace which includes the name of
755 * the custom field which value is to be retrieved.
756 *
757 * @return string|null
758 */
759 private function retrieve_cf_custom_field_name( $var_to_replace ) {
760 $replacement = null;
761
762 if ( is_string( $var_to_replace ) && $var_to_replace !== '' ) {
763 $field = substr( $var_to_replace, 3 );
764 if ( ! empty( $this->args->ID ) ) {
765 // Post meta can be arrays and in this case we need to exclude them.
766 $name = get_post_meta( $this->args->ID, $field, true );
767 if ( $name !== '' && ! is_array( $name ) ) {
768 $replacement = sanitize_text_field( $name );
769 }
770 }
771 elseif ( ! empty( $this->args->term_id ) ) {
772 $name = get_term_meta( $this->args->term_id, $field, true );
773 if ( $name !== '' ) {
774 $replacement = sanitize_text_field( $name );
775 }
776 }
777 }
778
779 return $replacement;
780 }
781
782 /**
783 * Retrieve a post/page/cpt's custom taxonomies for use as replacement string.
784 *
785 * @param string $var_to_replace The complete variable to replace which includes the name of
786 * the custom taxonomy which value(s) is to be retrieved.
787 * @param bool $single Whether to retrieve only the first or all values for the taxonomy.
788 *
789 * @return string|null
790 */
791 private function retrieve_ct_custom_tax_name( $var_to_replace, $single = false ) {
792 $replacement = null;
793
794 if ( ( is_string( $var_to_replace ) && $var_to_replace !== '' ) && ! empty( $this->args->ID ) ) {
795 $tax = substr( $var_to_replace, 3 );
796 $name = $this->get_terms( $this->args->ID, $tax, $single );
797 if ( $name !== '' ) {
798 $replacement = $name;
799 }
800 }
801
802 return $replacement;
803 }
804
805 /**
806 * Retrieve a post/page/cpt's custom taxonomies description for use as replacement string.
807 *
808 * @param string $var_to_replace The complete variable to replace which includes the name of
809 * the custom taxonomy which description is to be retrieved.
810 *
811 * @return string|null
812 */
813 private function retrieve_ct_desc_custom_tax_name( $var_to_replace ) {
814 $replacement = null;
815
816 if ( is_string( $var_to_replace ) && $var_to_replace !== '' ) {
817 $tax = substr( $var_to_replace, 8 );
818 if ( ! empty( $this->args->ID ) ) {
819 $terms = get_the_terms( $this->args->ID, $tax );
820 if ( is_array( $terms ) && $terms !== [] ) {
821 $term = current( $terms );
822 $term_desc = get_term_field( 'description', $term->term_id, $tax );
823 if ( $term_desc !== '' ) {
824 $replacement = wp_strip_all_tags( $term_desc );
825 }
826 }
827 }
828 }
829
830 return $replacement;
831 }
832
833 /**
834 * Retrieve the current date for use as replacement string.
835 *
836 * The `$replacement` variable is static because it doesn't change depending
837 * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482.
838 *
839 * @return string The formatted current date.
840 */
841 private function retrieve_currentdate() {
842 static $replacement;
843
844 if ( ! isset( $replacement ) ) {
845 $replacement = date_i18n( get_option( 'date_format' ) );
846 }
847
848 return $replacement;
849 }
850
851 /**
852 * Retrieve the current day for use as replacement string.
853 *
854 * The `$replacement` variable is static because it doesn't change depending
855 * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482.
856 *
857 * @return string The current day.
858 */
859 private function retrieve_currentday() {
860 static $replacement;
861
862 if ( ! isset( $replacement ) ) {
863 $replacement = date_i18n( 'j' );
864 }
865
866 return $replacement;
867 }
868
869 /**
870 * Retrieve the current month for use as replacement string.
871 *
872 * The `$replacement` variable is static because it doesn't change depending
873 * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482.
874 *
875 * @return string The current month.
876 */
877 private function retrieve_currentmonth() {
878 static $replacement;
879
880 if ( ! isset( $replacement ) ) {
881 $replacement = date_i18n( 'F' );
882 }
883
884 return $replacement;
885 }
886
887 /**
888 * Retrieve the current time for use as replacement string.
889 *
890 * The `$replacement` variable is static because it doesn't change depending
891 * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482.
892 *
893 * @return string The formatted current time.
894 */
895 private function retrieve_currenttime() {
896 static $replacement;
897
898 if ( ! isset( $replacement ) ) {
899 $replacement = date_i18n( get_option( 'time_format' ) );
900 }
901
902 return $replacement;
903 }
904
905 /**
906 * Retrieve the current year for use as replacement string.
907 *
908 * The `$replacement` variable is static because it doesn't change depending
909 * on the context. See https://github.com/Yoast/wordpress-seo/pull/1172#issuecomment-46019482.
910 *
911 * @return string The current year.
912 */
913 private function retrieve_currentyear() {
914 static $replacement;
915
916 if ( ! isset( $replacement ) ) {
917 $replacement = date_i18n( 'Y' );
918 }
919
920 return $replacement;
921 }
922
923 /**
924 * Retrieve the post/page/cpt's focus keyword for use as replacement string.
925 *
926 * @return string|null
927 */
928 private function retrieve_focuskw() {
929 // Retrieve focuskw from a Post.
930 if ( ! empty( $this->args->ID ) ) {
931 $focus_kw = WPSEO_Meta::get_value( 'focuskw', $this->args->ID );
932 if ( $focus_kw !== '' ) {
933 return $focus_kw;
934 }
935
936 return null;
937 }
938
939 // Retrieve focuskw from a Term.
940 if ( ! empty( $this->args->term_id ) ) {
941 $focus_kw = WPSEO_Taxonomy_Meta::get_term_meta( $this->args->term_id, $this->args->taxonomy, 'focuskw' );
942 if ( $focus_kw !== '' ) {
943 return $focus_kw;
944 }
945 }
946
947 return null;
948 }
949
950 /**
951 * Retrieve the post/page/cpt ID for use as replacement string.
952 *
953 * @return string|null
954 */
955 private function retrieve_id() {
956 $replacement = null;
957
958 if ( ! empty( $this->args->ID ) ) {
959 // The post/page/cpt ID is an integer, let's cast to string.
960 $replacement = (string) $this->args->ID;
961 }
962
963 return $replacement;
964 }
965
966 /**
967 * Retrieve the post/page/cpt modified time for use as replacement string.
968 *
969 * @return string|null
970 */
971 private function retrieve_modified() {
972 $replacement = null;
973
974 if ( ! empty( $this->args->post_modified ) ) {
975 $replacement = YoastSEO()->helpers->date->format_translated( $this->args->post_modified, get_option( 'date_format' ) );
976 }
977
978 return $replacement;
979 }
980
981 /**
982 * Retrieve the post/page/cpt author's "nice name" for use as replacement string.
983 *
984 * @return string|null
985 */
986 private function retrieve_name() {
987 $replacement = null;
988
989 $user_id = (int) $this->retrieve_userid();
990 $name = get_the_author_meta( 'display_name', $user_id );
991 if ( $name !== '' ) {
992 $replacement = $name;
993 }
994
995 return $replacement;
996 }
997
998 /**
999 * Retrieve the post/page/cpt author's users description for use as a replacement string.
1000 *
1001 * @return string|null
1002 */
1003 private function retrieve_user_description() {
1004 $replacement = null;
1005
1006 $user_id = (int) $this->retrieve_userid();
1007 $description = get_the_author_meta( 'description', $user_id );
1008 if ( $description !== '' ) {
1009 $replacement = $description;
1010 }
1011
1012 return $replacement;
1013 }
1014
1015 /**
1016 * Retrieve the current page number with context (i.e. 'page 2 of 4') for use as replacement string.
1017 *
1018 * @return string
1019 */
1020 private function retrieve_page() {
1021 $replacement = null;
1022
1023 $max = $this->determine_pagenumbering( 'max' );
1024 $nr = $this->determine_pagenumbering( 'nr' );
1025 $sep = $this->retrieve_sep();
1026
1027 if ( $max > 1 && $nr > 1 ) {
1028 /* translators: 1: current page number, 2: total number of pages. */
1029 $replacement = sprintf( $sep . ' ' . __( 'Page %1$d of %2$d', 'wordpress-seo' ), $nr, $max );
1030 }
1031
1032 return $replacement;
1033 }
1034
1035 /**
1036 * Retrieve the current page number for use as replacement string.
1037 *
1038 * @return string|null
1039 */
1040 private function retrieve_pagenumber() {
1041 $replacement = null;
1042
1043 $nr = $this->determine_pagenumbering( 'nr' );
1044 if ( isset( $nr ) && $nr > 0 ) {
1045 $replacement = (string) $nr;
1046 }
1047
1048 return $replacement;
1049 }
1050
1051 /**
1052 * Retrieve the current page total for use as replacement string.
1053 *
1054 * @return string|null
1055 */
1056 private function retrieve_pagetotal() {
1057 $replacement = null;
1058
1059 $max = $this->determine_pagenumbering( 'max' );
1060 if ( isset( $max ) && $max > 0 ) {
1061 $replacement = (string) $max;
1062 }
1063
1064 return $replacement;
1065 }
1066
1067 /**
1068 * Retrieve the post type plural label for use as replacement string.
1069 *
1070 * @return string|null
1071 */
1072 private function retrieve_pt_plural() {
1073 $replacement = null;
1074
1075 $name = $this->determine_pt_names( 'plural' );
1076 if ( isset( $name ) && $name !== '' ) {
1077 $replacement = $name;
1078 }
1079
1080 return $replacement;
1081 }
1082
1083 /**
1084 * Retrieve the post type single label for use as replacement string.
1085 *
1086 * @return string|null
1087 */
1088 private function retrieve_pt_single() {
1089 $replacement = null;
1090
1091 $name = $this->determine_pt_names( 'single' );
1092 if ( isset( $name ) && $name !== '' ) {
1093 $replacement = $name;
1094 }
1095
1096 return $replacement;
1097 }
1098
1099 /**
1100 * Retrieve the slug which caused the 404 for use as replacement string.
1101 *
1102 * @return string|null
1103 */
1104 private function retrieve_term404() {
1105 $replacement = null;
1106
1107 if ( $this->args->term404 !== '' ) {
1108 $replacement = sanitize_text_field( str_replace( '-', ' ', $this->args->term404 ) );
1109 }
1110 else {
1111 $error_request = get_query_var( 'pagename' );
1112 if ( $error_request !== '' ) {
1113 $replacement = sanitize_text_field( str_replace( '-', ' ', $error_request ) );
1114 }
1115 else {
1116 $error_request = get_query_var( 'name' );
1117 if ( $error_request !== '' ) {
1118 $replacement = sanitize_text_field( str_replace( '-', ' ', $error_request ) );
1119 }
1120 }
1121 }
1122
1123 return $replacement;
1124 }
1125
1126 /**
1127 * Retrieve the post/page/cpt author's user id for use as replacement string.
1128 *
1129 * @return string
1130 */
1131 private function retrieve_userid() {
1132 // The user ID is an integer, let's cast to string.
1133 $replacement = ! empty( $this->args->post_author ) ? (string) $this->args->post_author : (string) get_query_var( 'author' );
1134
1135 return $replacement;
1136 }
1137
1138 /**
1139 * Retrieve the post/page/cpt's published year for use as replacement string.
1140 *
1141 * @return string|null
1142 */
1143 private function retrieve_post_year() {
1144 if ( empty( $this->args->ID ) ) {
1145 return null;
1146 }
1147
1148 return get_the_date( 'Y', $this->args->ID );
1149 }
1150
1151 /**
1152 * Retrieve the post/page/cpt's published month for use as replacement string.
1153 *
1154 * @return string|null
1155 */
1156 private function retrieve_post_month() {
1157 if ( empty( $this->args->ID ) ) {
1158 return null;
1159 }
1160
1161 return get_the_date( 'F', $this->args->ID );
1162 }
1163
1164 /**
1165 * Retrieve the post/page/cpt's published day for use as replacement string.
1166 *
1167 * @return string|null
1168 */
1169 private function retrieve_post_day() {
1170 if ( empty( $this->args->ID ) ) {
1171 return null;
1172 }
1173
1174 return get_the_date( 'd', $this->args->ID );
1175 }
1176
1177 /**
1178 * Retrieve the post/page/cpt author's first name for use as replacement string.
1179 *
1180 * @return string|null
1181 */
1182 private function retrieve_author_first_name() {
1183 $replacement = null;
1184
1185 $user_id = (int) $this->retrieve_userid();
1186 $name = get_the_author_meta( 'first_name', $user_id );
1187 if ( $name !== '' ) {
1188 $replacement = $name;
1189 }
1190
1191 return $replacement;
1192 }
1193
1194 /**
1195 * Retrieve the post/page/cpt author's last name for use as replacement string.
1196 *
1197 * @return string|null
1198 */
1199 private function retrieve_author_last_name() {
1200 $replacement = null;
1201
1202 $user_id = (int) $this->retrieve_userid();
1203 $name = get_the_author_meta( 'last_name', $user_id );
1204 if ( $name !== '' ) {
1205 $replacement = $name;
1206 }
1207
1208 return $replacement;
1209 }
1210
1211 /**
1212 * Retrieve the post/page/cpt permalink for use as replacement string.
1213 *
1214 * @return string|null
1215 */
1216 private function retrieve_permalink() {
1217 if ( empty( $this->args->ID ) ) {
1218 return null;
1219 }
1220
1221 return get_permalink( $this->args->ID );
1222 }
1223
1224 /**
1225 * Retrieve the post/page/cpt content for use as replacement string.
1226 *
1227 * @return string|null
1228 */
1229 private function retrieve_post_content() {
1230 $replacement = null;
1231
1232 // The check `post_password_required` is because content must be hidden for a post with a password.
1233 if ( ! empty( $this->args->ID ) && $this->args->post_content !== '' && ! post_password_required( $this->args->ID ) ) {
1234 $content = strip_shortcodes( $this->args->post_content );
1235 $replacement = wp_strip_all_tags( $content );
1236 }
1237
1238 return $replacement;
1239 }
1240
1241 /**
1242 * Retrieve the current or first category title. To be used for import data from AIOSEO.
1243 * The code derives from AIOSEO's way of dealing with that var, so we can ensure 100% seamless transition.
1244 *
1245 * @return string|null
1246 */
1247 private function retrieve_category_title() {
1248 if ( empty( $this->args ) || empty( $this->args->ID ) ) {
1249 return null;
1250 }
1251 $post_id = $this->args->ID;
1252
1253 $post = get_post( $post_id );
1254 $taxonomies = get_object_taxonomies( $post, 'objects' );
1255
1256 foreach ( $taxonomies as $taxonomy_slug => $taxonomy ) {
1257 if ( ! $taxonomy->hierarchical ) {
1258 continue;
1259 }
1260 $post_terms = get_the_terms( $post_id, $taxonomy_slug );
1261 if ( is_array( $post_terms ) && count( $post_terms ) > 0 ) {
1262 // AiOSEO takes the name of whatever the first hierarchical taxonomy is.
1263 $term = reset( $post_terms );
1264 if ( $term ) {
1265 return $term->name;
1266 }
1267 }
1268 }
1269
1270 return null;
1271 }
1272
1273 /* *********************** HELP TEXT RELATED ************************** */
1274
1275 /**
1276 * Set the help text for a user/plugin/theme defined extra variable.
1277 *
1278 * @param string $type Type of variable: 'basic' or 'advanced'.
1279 * @param WPSEO_Replacement_Variable $replacement_variable The replacement variable to register.
1280 *
1281 * @return void
1282 */
1283 private static function register_help_text( $type, WPSEO_Replacement_Variable $replacement_variable ) {
1284 $identifier = $replacement_variable->get_variable();
1285
1286 if ( ( is_string( $type ) && in_array( $type, [ 'basic', 'advanced' ], true ) )
1287 && ( $identifier !== '' && ! isset( self::$help_texts[ $type ][ $identifier ] ) )
1288 ) {
1289 self::$help_texts[ $type ][ $identifier ] = $replacement_variable;
1290 }
1291 }
1292
1293 /**
1294 * Generates a list of replacement variables based on the help texts.
1295 *
1296 * @return array List of replace vars.
1297 */
1298 public function get_replacement_variables_with_labels() {
1299 self::setup_statics_once();
1300
1301 $custom_variables = [];
1302 foreach ( array_merge( WPSEO_Custom_Fields::get_custom_fields(), WPSEO_Custom_Taxonomies::get_custom_taxonomies() ) as $custom_variable ) {
1303 $custom_variables[ $custom_variable ] = new WPSEO_Replacement_Variable( $custom_variable, $this->get_label( $custom_variable ), '' );
1304 }
1305
1306 $replacement_variables = array_filter(
1307 array_merge( self::$help_texts['basic'], self::$help_texts['advanced'] ),
1308 [ $this, 'is_not_prefixed' ],
1309 ARRAY_FILTER_USE_KEY,
1310 );
1311
1312 $hidden = $this->get_hidden_replace_vars();
1313
1314 return array_values(
1315 array_map(
1316 static function ( WPSEO_Replacement_Variable $replacement_variable ) use ( $hidden ) {
1317 $name = $replacement_variable->get_variable();
1318
1319 return [
1320 'name' => $name,
1321 'value' => '',
1322 'label' => $replacement_variable->get_label(),
1323 'hidden' => in_array( $name, $hidden, true ),
1324 ];
1325 },
1326 array_merge( $replacement_variables, $custom_variables ),
1327 ),
1328 );
1329 }
1330
1331 /**
1332 * Generates a list of replacement variables based on the help texts.
1333 *
1334 * @return array List of replace vars.
1335 */
1336 public function get_replacement_variables_list() {
1337 self::setup_statics_once();
1338
1339 $replacement_variables = array_merge(
1340 $this->get_replacement_variables(),
1341 WPSEO_Custom_Fields::get_custom_fields(),
1342 WPSEO_Custom_Taxonomies::get_custom_taxonomies(),
1343 );
1344
1345 return array_map( [ $this, 'format_replacement_variable' ], $replacement_variables );
1346 }
1347
1348 /**
1349 * Creates a merged associative array of both the basic and advanced help texts.
1350 *
1351 * @return array Array with the replacement variables.
1352 */
1353 private function get_replacement_variables() {
1354 $help_texts = array_merge( self::$help_texts['basic'], self::$help_texts['advanced'] );
1355
1356 return array_filter( array_keys( $help_texts ), [ $this, 'is_not_prefixed' ] );
1357 }
1358
1359 /**
1360 * Checks whether the replacement variable contains a `ct_` or `cf_` prefix, because they follow different logic.
1361 *
1362 * @param string $replacement_variable The replacement variable.
1363 *
1364 * @return bool True when the replacement variable is not prefixed.
1365 */
1366 private function is_not_prefixed( $replacement_variable ) {
1367 $prefixes = [ 'cf_', 'ct_' ];
1368 $prefix = $this->get_prefix( $replacement_variable );
1369
1370 return ! in_array( $prefix, $prefixes, true );
1371 }
1372
1373 /**
1374 * Strip the prefix from a replacement variable name.
1375 *
1376 * @param string $replacement_variable The replacement variable.
1377 *
1378 * @return string The replacement variable name without the prefix.
1379 */
1380 private function strip_prefix( $replacement_variable ) {
1381 return substr( $replacement_variable, 3 );
1382 }
1383
1384 /**
1385 * Gets the prefix from a replacement variable name.
1386 *
1387 * @param string $replacement_variable The replacement variable.
1388 *
1389 * @return string The prefix of the replacement variable.
1390 */
1391 private function get_prefix( $replacement_variable ) {
1392 return substr( $replacement_variable, 0, 3 );
1393 }
1394
1395 /**
1396 * Strips 'desc_' if present, and appends ' description' at the end.
1397 *
1398 * @param string $label The replacement variable.
1399 *
1400 * @return string The altered replacement variable name.
1401 */
1402 private function handle_description( $label ) {
1403 if ( strpos( $label, 'desc_' ) === 0 ) {
1404 return substr( $label, 5 ) . ' description';
1405 }
1406
1407 return $label;
1408 }
1409
1410 /**
1411 * Creates a label for prefixed replacement variables that matches the format in the editors.
1412 *
1413 * @param string $replacement_variable The replacement variable.
1414 *
1415 * @return string The replacement variable label.
1416 */
1417 private function get_label( $replacement_variable ) {
1418 $prefix = $this->get_prefix( $replacement_variable );
1419 if ( $prefix === 'cf_' ) {
1420 return $this->strip_prefix( $replacement_variable ) . ' (custom field)';
1421 }
1422
1423 if ( $prefix === 'ct_' ) {
1424 $label = $this->strip_prefix( $replacement_variable );
1425 $label = $this->handle_description( $label );
1426 return ucfirst( $label . ' (custom taxonomy)' );
1427 }
1428
1429 if ( $prefix === 'pt_' ) {
1430 if ( $replacement_variable === 'pt_single' ) {
1431 return 'Post type (singular)';
1432 }
1433
1434 return 'Post type (plural)';
1435 }
1436
1437 return '';
1438 }
1439
1440 /**
1441 * Formats the replacement variables.
1442 *
1443 * @param string $replacement_variable The replacement variable to format.
1444 *
1445 * @return array The formatted replacement variable.
1446 */
1447 private function format_replacement_variable( $replacement_variable ) {
1448 return [
1449 'name' => $replacement_variable,
1450 'value' => '',
1451 'label' => $this->get_label( $replacement_variable ),
1452 ];
1453 }
1454
1455 /**
1456 * Set/translate the help texts for the WPSEO standard basic variables.
1457 *
1458 * @return void
1459 */
1460 private static function set_basic_help_texts() {
1461 /* translators: %s: wp_title() function. */
1462 $separator_description = __( 'The separator defined in your theme\'s %s tag.', 'wordpress-seo' );
1463 $separator_description = sprintf(
1464 $separator_description,
1465 // '<code>wp_title()</code>'
1466 'wp_title()',
1467 );
1468
1469 $replacement_variables = [
1470 new WPSEO_Replacement_Variable( 'date', __( 'Date', 'wordpress-seo' ), __( 'Replaced with the date of the post/page', 'wordpress-seo' ) ),
1471 new WPSEO_Replacement_Variable( 'title', __( 'Title', 'wordpress-seo' ), __( 'Replaced with the title of the post/page', 'wordpress-seo' ) ),
1472 new WPSEO_Replacement_Variable( 'parent_title', __( 'Parent title', 'wordpress-seo' ), __( 'Replaced with the title of the parent page of the current page', 'wordpress-seo' ) ),
1473 new WPSEO_Replacement_Variable( 'archive_title', __( 'Archive title', 'wordpress-seo' ), __( 'Replaced with the normal title for an archive generated by WordPress', 'wordpress-seo' ) ),
1474 new WPSEO_Replacement_Variable( 'sitename', __( 'Site title', 'wordpress-seo' ), __( 'The site\'s name', 'wordpress-seo' ) ),
1475 new WPSEO_Replacement_Variable( 'sitedesc', __( 'Tagline', 'wordpress-seo' ), __( 'The site\'s tagline', 'wordpress-seo' ) ),
1476 new WPSEO_Replacement_Variable( 'excerpt', __( 'Excerpt', 'wordpress-seo' ), __( 'Replaced with the post/page excerpt (or auto-generated if it does not exist)', 'wordpress-seo' ) ),
1477 new WPSEO_Replacement_Variable( 'excerpt_only', __( 'Excerpt only', 'wordpress-seo' ), __( 'Replaced with the post/page excerpt (without auto-generation)', 'wordpress-seo' ) ),
1478 new WPSEO_Replacement_Variable( 'tag', __( 'Tag', 'wordpress-seo' ), __( 'Replaced with the current tag/tags', 'wordpress-seo' ) ),
1479 new WPSEO_Replacement_Variable( 'category', __( 'Category', 'wordpress-seo' ), __( 'Replaced with the post categories (comma separated)', 'wordpress-seo' ) ),
1480 new WPSEO_Replacement_Variable( 'primary_category', __( 'Primary category', 'wordpress-seo' ), __( 'Replaced with the primary category of the post/page', 'wordpress-seo' ) ),
1481 new WPSEO_Replacement_Variable( 'category_description', __( 'Category description', 'wordpress-seo' ), __( 'Replaced with the category description', 'wordpress-seo' ) ),
1482 new WPSEO_Replacement_Variable( 'tag_description', __( 'Tag description', 'wordpress-seo' ), __( 'Replaced with the tag description', 'wordpress-seo' ) ),
1483 new WPSEO_Replacement_Variable( 'term_description', __( 'Term description', 'wordpress-seo' ), __( 'Replaced with the term description', 'wordpress-seo' ) ),
1484 new WPSEO_Replacement_Variable( 'term_title', __( 'Term title', 'wordpress-seo' ), __( 'Replaced with the term name', 'wordpress-seo' ) ),
1485 new WPSEO_Replacement_Variable( 'searchphrase', __( 'Search phrase', 'wordpress-seo' ), __( 'Replaced with the current search phrase', 'wordpress-seo' ) ),
1486 new WPSEO_Replacement_Variable( 'term_hierarchy', __( 'Term hierarchy', 'wordpress-seo' ), __( 'Replaced with the term ancestors hierarchy', 'wordpress-seo' ) ),
1487 new WPSEO_Replacement_Variable( 'sep', __( 'Separator', 'wordpress-seo' ), $separator_description ),
1488 new WPSEO_Replacement_Variable( 'currentdate', __( 'Current date', 'wordpress-seo' ), __( 'Replaced with the current date', 'wordpress-seo' ) ),
1489 new WPSEO_Replacement_Variable( 'currentyear', __( 'Current year', 'wordpress-seo' ), __( 'Replaced with the current year', 'wordpress-seo' ) ),
1490 new WPSEO_Replacement_Variable( 'currentmonth', __( 'Current month', 'wordpress-seo' ), __( 'Replaced with the current month', 'wordpress-seo' ) ),
1491 new WPSEO_Replacement_Variable( 'currentday', __( 'Current day', 'wordpress-seo' ), __( 'Replaced with the current day', 'wordpress-seo' ) ),
1492 new WPSEO_Replacement_Variable( 'post_year', __( 'Post year', 'wordpress-seo' ), __( 'Replaced with the year the post was published', 'wordpress-seo' ) ),
1493 new WPSEO_Replacement_Variable( 'post_month', __( 'Post month', 'wordpress-seo' ), __( 'Replaced with the month the post was published', 'wordpress-seo' ) ),
1494 new WPSEO_Replacement_Variable( 'post_day', __( 'Post day', 'wordpress-seo' ), __( 'Replaced with the day the post was published', 'wordpress-seo' ) ),
1495 new WPSEO_Replacement_Variable( 'author_first_name', __( 'Author first name', 'wordpress-seo' ), __( 'Replaced with the first name of the author', 'wordpress-seo' ) ),
1496 new WPSEO_Replacement_Variable( 'author_last_name', __( 'Author last name', 'wordpress-seo' ), __( 'Replaced with the last name of the author', 'wordpress-seo' ) ),
1497 new WPSEO_Replacement_Variable( 'permalink', __( 'Permalink', 'wordpress-seo' ), __( 'Replaced with the permalink', 'wordpress-seo' ) ),
1498 new WPSEO_Replacement_Variable( 'post_content', __( 'Post Content', 'wordpress-seo' ), __( 'Replaced with the post content', 'wordpress-seo' ) ),
1499 new WPSEO_Replacement_Variable( 'category_title', __( 'Category Title', 'wordpress-seo' ), __( 'Current or first category title', 'wordpress-seo' ) ),
1500 ];
1501
1502 foreach ( $replacement_variables as $replacement_variable ) {
1503 self::register_help_text( 'basic', $replacement_variable );
1504 }
1505 }
1506
1507 /**
1508 * Set/translate the help texts for the WPSEO standard advanced variables.
1509 *
1510 * @return void
1511 */
1512 private static function set_advanced_help_texts() {
1513 $replacement_variables = [
1514 new WPSEO_Replacement_Variable( 'pt_single', __( 'Post type (singular)', 'wordpress-seo' ), __( 'Replaced with the content type single label', 'wordpress-seo' ) ),
1515 new WPSEO_Replacement_Variable( 'pt_plural', __( 'Post type (plural)', 'wordpress-seo' ), __( 'Replaced with the content type plural label', 'wordpress-seo' ) ),
1516 new WPSEO_Replacement_Variable( 'modified', __( 'Modified', 'wordpress-seo' ), __( 'Replaced with the post/page modified time', 'wordpress-seo' ) ),
1517 new WPSEO_Replacement_Variable( 'id', __( 'ID', 'wordpress-seo' ), __( 'Replaced with the post/page ID', 'wordpress-seo' ) ),
1518 new WPSEO_Replacement_Variable( 'name', __( 'Name', 'wordpress-seo' ), __( 'Replaced with the post/page author\'s \'nicename\'', 'wordpress-seo' ) ),
1519 new WPSEO_Replacement_Variable( 'user_description', __( 'User description', 'wordpress-seo' ), __( 'Replaced with the post/page author\'s \'Biographical Info\'', 'wordpress-seo' ) ),
1520 new WPSEO_Replacement_Variable( 'page', __( 'Page', 'wordpress-seo' ), __( 'Replaced with the current page number with context (i.e. page 2 of 4)', 'wordpress-seo' ) ),
1521 new WPSEO_Replacement_Variable( 'pagetotal', __( 'Pagetotal', 'wordpress-seo' ), __( 'Replaced with the current page total', 'wordpress-seo' ) ),
1522 new WPSEO_Replacement_Variable( 'pagenumber', __( 'Pagenumber', 'wordpress-seo' ), __( 'Replaced with the current page number', 'wordpress-seo' ) ),
1523 new WPSEO_Replacement_Variable( 'caption', __( 'Caption', 'wordpress-seo' ), __( 'Attachment caption', 'wordpress-seo' ) ),
1524 new WPSEO_Replacement_Variable( 'focuskw', __( 'Focus keyword', 'wordpress-seo' ), __( 'Replaced with the posts focus keyphrase', 'wordpress-seo' ) ),
1525 new WPSEO_Replacement_Variable( 'term404', __( 'Term404', 'wordpress-seo' ), __( 'Replaced with the slug which caused the 404', 'wordpress-seo' ) ),
1526 new WPSEO_Replacement_Variable( 'cf_<custom-field-name>', '<custom-field-name> ' . __( '(custom field)', 'wordpress-seo' ), __( 'Replaced with a posts custom field value', 'wordpress-seo' ) ),
1527 new WPSEO_Replacement_Variable( 'ct_<custom-tax-name>', '<custom-tax-name> ' . __( '(custom taxonomy)', 'wordpress-seo' ), __( 'Replaced with a posts custom taxonomies, comma separated.', 'wordpress-seo' ) ),
1528 new WPSEO_Replacement_Variable( 'ct_desc_<custom-tax-name>', '<custom-tax-name> ' . __( 'description (custom taxonomy)', 'wordpress-seo' ), __( 'Replaced with a custom taxonomies description', 'wordpress-seo' ) ),
1529 ];
1530
1531 foreach ( $replacement_variables as $replacement_variable ) {
1532 self::register_help_text( 'advanced', $replacement_variable );
1533 }
1534 }
1535
1536 /* *********************** GENERAL HELPER METHODS ************************** */
1537
1538 /**
1539 * Remove the '%%' delimiters from a variable string.
1540 *
1541 * @param string $text Variable string to be cleaned.
1542 *
1543 * @return string
1544 */
1545 private static function remove_var_delimiter( $text ) {
1546 return trim( $text, '%' );
1547 }
1548
1549 /**
1550 * Add the '%%' delimiters to a variable string.
1551 *
1552 * @param string $text Variable string to be delimited.
1553 *
1554 * @return string
1555 */
1556 private static function add_var_delimiter( $text ) {
1557 return '%%' . $text . '%%';
1558 }
1559
1560 /**
1561 * Retrieve a post's terms, comma delimited.
1562 *
1563 * @param int $id ID of the post to get the terms for.
1564 * @param string $taxonomy The taxonomy to get the terms for this post from.
1565 * @param bool $return_single If true, return the first term.
1566 *
1567 * @return string Either a single term or a comma delimited string of terms.
1568 */
1569 public function get_terms( $id, $taxonomy, $return_single = false ) {
1570 $output = '';
1571
1572 // If we're on a specific tag, category or taxonomy page, use that.
1573 if ( ! empty( $this->args->term_id ) ) {
1574 $output = $this->args->name;
1575 }
1576 elseif ( ! empty( $id ) && ! empty( $taxonomy ) ) {
1577 $terms = get_the_terms( $id, $taxonomy );
1578 if ( is_array( $terms ) && $terms !== [] ) {
1579 foreach ( $terms as $term ) {
1580 if ( $return_single ) {
1581 $output = $term->name;
1582 break;
1583 }
1584 else {
1585 $output .= $term->name . ', ';
1586 }
1587 }
1588 $output = rtrim( trim( $output ), ',' );
1589 }
1590 }
1591 unset( $terms, $term );
1592
1593 /**
1594 * Allows filtering of the terms list used to replace %%category%%, %%tag%%
1595 * and %%ct_<custom-tax-name>%% variables.
1596 *
1597 * @param string $output Comma-delimited string containing the terms.
1598 * @param string $taxonomy The taxonomy of the terms.
1599 */
1600 return apply_filters( 'wpseo_terms', $output, $taxonomy );
1601 }
1602
1603 /**
1604 * Gets a taxonomy term hierarchy including the term to get the parents for.
1605 *
1606 * @return string
1607 */
1608 private function get_term_hierarchy() {
1609 if ( ! is_taxonomy_hierarchical( $this->args->taxonomy ) ) {
1610 return '';
1611 }
1612
1613 $separator = ' ' . $this->retrieve_sep() . ' ';
1614
1615 $args = [
1616 'format' => 'name',
1617 'separator' => $separator,
1618 'link' => false,
1619 'inclusive' => true,
1620 ];
1621
1622 return rtrim(
1623 get_term_parents_list( $this->args->term_id, $this->args->taxonomy, $args ),
1624 $separator,
1625 );
1626 }
1627
1628 /**
1629 * Retrieves the term ancestors hierarchy.
1630 *
1631 * @return string|null The term ancestors hierarchy.
1632 */
1633 private function retrieve_term_hierarchy() {
1634 $replacement = null;
1635
1636 if ( ! empty( $this->args->term_id ) && ! empty( $this->args->taxonomy ) ) {
1637 $hierarchy = $this->get_term_hierarchy();
1638
1639 if ( $hierarchy !== '' ) {
1640 $replacement = esc_html( $hierarchy );
1641 }
1642 }
1643
1644 return $replacement;
1645 }
1646 }
1647