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