class-wpseo-replace-vars.php 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400
  1. <?php
  2. /**
  3. * WPSEO plugin file.
  4. *
  5. * @package WPSEO\Internals
  6. * @since 1.5.4
  7. */
  8. // Avoid direct calls to this file.
  9. if ( ! defined( 'WPSEO_VERSION' ) ) {
  10. header( 'Status: 403 Forbidden' );
  11. header( 'HTTP/1.1 403 Forbidden' );
  12. exit();
  13. }
  14. /**
  15. * Class: WPSEO_Replace_Vars.
  16. *
  17. * This class implements the replacing of `%%variable_placeholders%%` with their real value based on the current
  18. * requested page/post/cpt/etc in text strings.
  19. */
  20. class WPSEO_Replace_Vars {
  21. /**
  22. * Default post/page/cpt information.
  23. *
  24. * @var array
  25. */
  26. protected $defaults = [
  27. 'ID' => '',
  28. 'name' => '',
  29. 'post_author' => '',
  30. 'post_content' => '',
  31. 'post_date' => '',
  32. 'post_excerpt' => '',
  33. 'post_modified' => '',
  34. 'post_title' => '',
  35. 'taxonomy' => '',
  36. 'term_id' => '',
  37. 'term404' => '',
  38. ];
  39. /**
  40. * Current post/page/cpt information.
  41. *
  42. * @var object
  43. */
  44. protected $args;
  45. /**
  46. * The date helper.
  47. *
  48. * @var WPSEO_Date_Helper
  49. */
  50. protected $date;
  51. /**
  52. * Help texts for use in WPSEO -> Search appearance tabs.
  53. *
  54. * @var array
  55. */
  56. protected static $help_texts = [];
  57. /**
  58. * Register of additional variable replacements registered by other plugins/themes.
  59. *
  60. * @var array
  61. */
  62. protected static $external_replacements = [];
  63. /**
  64. * Constructor.
  65. *
  66. * @return \WPSEO_Replace_Vars
  67. */
  68. public function __construct() {
  69. $this->date = new WPSEO_Date_Helper();
  70. }
  71. /**
  72. * Setup the help texts and external replacements as statics so they will be available to all instances.
  73. */
  74. public static function setup_statics_once() {
  75. if ( self::$help_texts === [] ) {
  76. self::set_basic_help_texts();
  77. self::set_advanced_help_texts();
  78. }
  79. if ( self::$external_replacements === [] ) {
  80. /**
  81. * Action: 'wpseo_register_extra_replacements' - Allows for registration of additional
  82. * variables to replace.
  83. */
  84. do_action( 'wpseo_register_extra_replacements' );
  85. }
  86. }
  87. /**
  88. * Register new replacement %%variables%%.
  89. * For use by other plugins/themes to register extra variables.
  90. *
  91. * @see wpseo_register_var_replacement() for a usage example.
  92. *
  93. * @param string $var The name of the variable to replace, i.e. '%%var%%'.
  94. * Note: the surrounding %% are optional.
  95. * @param mixed $replace_function Function or method to call to retrieve the replacement value for the variable.
  96. * Uses the same format as add_filter/add_action function parameter and
  97. * should *return* the replacement value. DON'T echo it.
  98. * @param string $type Type of variable: 'basic' or 'advanced', defaults to 'advanced'.
  99. * @param string $help_text Help text to be added to the help tab for this variable.
  100. *
  101. * @return bool Whether the replacement function was succesfully registered.
  102. */
  103. public static function register_replacement( $var, $replace_function, $type = 'advanced', $help_text = '' ) {
  104. $success = false;
  105. if ( is_string( $var ) && $var !== '' ) {
  106. $var = self::remove_var_delimiter( $var );
  107. if ( preg_match( '`^[A-Z0-9_-]+$`i', $var ) === false ) {
  108. 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 );
  109. }
  110. elseif ( strpos( $var, 'cf_' ) === 0 || strpos( $var, 'ct_' ) === 0 ) {
  111. 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 );
  112. }
  113. elseif ( ! method_exists( __CLASS__, 'retrieve_' . $var ) ) {
  114. if ( $var !== '' && ! isset( self::$external_replacements[ $var ] ) ) {
  115. self::$external_replacements[ $var ] = $replace_function;
  116. $replacement_variable = new WPSEO_Replacement_Variable( $var, $var, $help_text );
  117. self::register_help_text( $type, $replacement_variable );
  118. $success = true;
  119. }
  120. else {
  121. 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 );
  122. }
  123. }
  124. else {
  125. 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 );
  126. }
  127. }
  128. return $success;
  129. }
  130. /**
  131. * Replace `%%variable_placeholders%%` with their real value based on the current requested page/post/cpt/etc.
  132. *
  133. * @param string $string The string to replace the variables in.
  134. * @param array $args The object some of the replacement values might come from,
  135. * could be a post, taxonomy or term.
  136. * @param array $omit Variables that should not be replaced by this function.
  137. *
  138. * @return string
  139. */
  140. public function replace( $string, $args, $omit = [] ) {
  141. $string = wp_strip_all_tags( $string );
  142. // Let's see if we can bail super early.
  143. if ( strpos( $string, '%%' ) === false ) {
  144. return WPSEO_Utils::standardize_whitespace( $string );
  145. }
  146. $args = (array) $args;
  147. if ( isset( $args['post_content'] ) && ! empty( $args['post_content'] ) ) {
  148. $args['post_content'] = WPSEO_Utils::strip_shortcode( $args['post_content'] );
  149. }
  150. if ( isset( $args['post_excerpt'] ) && ! empty( $args['post_excerpt'] ) ) {
  151. $args['post_excerpt'] = WPSEO_Utils::strip_shortcode( $args['post_excerpt'] );
  152. }
  153. $this->args = (object) wp_parse_args( $args, $this->defaults );
  154. // Clean $omit array.
  155. if ( is_array( $omit ) && $omit !== [] ) {
  156. $omit = array_map( [ __CLASS__, 'remove_var_delimiter' ], $omit );
  157. }
  158. $replacements = [];
  159. if ( preg_match_all( '`%%([^%]+(%%single)?)%%?`iu', $string, $matches ) ) {
  160. $replacements = $this->set_up_replacements( $matches, $omit );
  161. }
  162. /**
  163. * Filter: 'wpseo_replacements' - Allow customization of the replacements before they are applied.
  164. *
  165. * @api array $replacements The replacements.
  166. *
  167. * @param array $args The object some of the replacement values might come from,
  168. * could be a post, taxonomy or term.
  169. */
  170. $replacements = apply_filters( 'wpseo_replacements', $replacements, $this->args );
  171. // Do the actual replacements.
  172. if ( is_array( $replacements ) && $replacements !== [] ) {
  173. $string = str_replace( array_keys( $replacements ), array_values( $replacements ), $string );
  174. }
  175. /**
  176. * Filter: 'wpseo_replacements_final' - Allow overruling of whether or not to remove placeholders
  177. * which didn't yield a replacement.
  178. *
  179. * @example <code>add_filter( 'wpseo_replacements_final', '__return_false' );</code>
  180. *
  181. * @api bool $final
  182. */
  183. if ( apply_filters( 'wpseo_replacements_final', true ) === true && ( isset( $matches[1] ) && is_array( $matches[1] ) ) ) {
  184. // Remove non-replaced variables.
  185. $remove = array_diff( $matches[1], $omit ); // Make sure the $omit variables do not get removed.
  186. $remove = array_map( [ __CLASS__, 'add_var_delimiter' ], $remove );
  187. $string = str_replace( $remove, '', $string );
  188. }
  189. // Undouble separators which have nothing between them, i.e. where a non-replaced variable was removed.
  190. if ( isset( $replacements['%%sep%%'] ) && ( is_string( $replacements['%%sep%%'] ) && $replacements['%%sep%%'] !== '' ) ) {
  191. $q_sep = preg_quote( $replacements['%%sep%%'], '`' );
  192. $string = preg_replace( '`' . $q_sep . '(?:\s*' . $q_sep . ')*`u', $replacements['%%sep%%'], $string );
  193. }
  194. // Remove superfluous whitespace.
  195. $string = WPSEO_Utils::standardize_whitespace( $string );
  196. return $string;
  197. }
  198. /**
  199. * Retrieve the replacements for the variables found.
  200. *
  201. * @param array $matches Variables found in the original string - regex result.
  202. * @param array $omit Variables that should not be replaced by this function.
  203. *
  204. * @return array Retrieved replacements - this might be a smaller array as some variables
  205. * may not yield a replacement in certain contexts.
  206. */
  207. private function set_up_replacements( $matches, $omit ) {
  208. $replacements = [];
  209. // @todo Figure out a way to deal with external functions starting with cf_/ct_.
  210. foreach ( $matches[1] as $k => $var ) {
  211. // Don't set up replacements which should be omitted.
  212. if ( in_array( $var, $omit, true ) ) {
  213. continue;
  214. }
  215. // Deal with variable variable names first.
  216. if ( strpos( $var, 'cf_' ) === 0 ) {
  217. $replacement = $this->retrieve_cf_custom_field_name( $var );
  218. }
  219. elseif ( strpos( $var, 'ct_desc_' ) === 0 ) {
  220. $replacement = $this->retrieve_ct_desc_custom_tax_name( $var );
  221. }
  222. elseif ( strpos( $var, 'ct_' ) === 0 ) {
  223. $single = ( isset( $matches[2][ $k ] ) && $matches[2][ $k ] !== '' ) ? true : false;
  224. $replacement = $this->retrieve_ct_custom_tax_name( $var, $single );
  225. } // Deal with non-variable variable names.
  226. elseif ( method_exists( $this, 'retrieve_' . $var ) ) {
  227. $method_name = 'retrieve_' . $var;
  228. $replacement = $this->$method_name();
  229. } // Deal with externally defined variable names.
  230. elseif ( isset( self::$external_replacements[ $var ] ) && ! is_null( self::$external_replacements[ $var ] ) ) {
  231. $replacement = call_user_func( self::$external_replacements[ $var ], $var, $this->args );
  232. }
  233. // Replacement retrievals can return null if no replacement can be determined, root those outs.
  234. if ( isset( $replacement ) ) {
  235. $var = self::add_var_delimiter( $var );
  236. $replacements[ $var ] = $replacement;
  237. }
  238. unset( $replacement, $single, $method_name );
  239. }
  240. return $replacements;
  241. }
  242. /* *********************** BASIC VARIABLES ************************** */
  243. /**
  244. * Retrieve the post/cpt categories (comma separated) for use as replacement string.
  245. *
  246. * @return string|null
  247. */
  248. private function retrieve_category() {
  249. $replacement = null;
  250. if ( ! empty( $this->args->ID ) ) {
  251. $cat = $this->get_terms( $this->args->ID, 'category' );
  252. if ( $cat !== '' ) {
  253. $replacement = $cat;
  254. }
  255. }
  256. if ( ( ! isset( $replacement ) || $replacement === '' ) && ( isset( $this->args->cat_name ) && ! empty( $this->args->cat_name ) ) ) {
  257. $replacement = $this->args->cat_name;
  258. }
  259. return $replacement;
  260. }
  261. /**
  262. * Retrieve the category description for use as replacement string.
  263. *
  264. * @return string|null
  265. */
  266. private function retrieve_category_description() {
  267. return $this->retrieve_term_description();
  268. }
  269. /**
  270. * Retrieve the date of the post/page/cpt for use as replacement string.
  271. *
  272. * @return string|null
  273. */
  274. private function retrieve_date() {
  275. $replacement = null;
  276. if ( $this->args->post_date !== '' ) {
  277. $replacement = $this->date->format_translated( $this->args->post_date, get_option( 'date_format' ) );
  278. }
  279. else {
  280. if ( get_query_var( 'day' ) && get_query_var( 'day' ) !== '' ) {
  281. $replacement = get_the_date();
  282. }
  283. else {
  284. if ( single_month_title( ' ', false ) && single_month_title( ' ', false ) !== '' ) {
  285. $replacement = single_month_title( ' ', false );
  286. }
  287. elseif ( get_query_var( 'year' ) !== '' ) {
  288. $replacement = get_query_var( 'year' );
  289. }
  290. }
  291. }
  292. return $replacement;
  293. }
  294. /**
  295. * Retrieve the post/page/cpt excerpt for use as replacement string.
  296. * The excerpt will be auto-generated if it does not exist.
  297. *
  298. * @return string|null
  299. */
  300. private function retrieve_excerpt() {
  301. $replacement = null;
  302. // The check `post_password_required` is because excerpt must be hidden for a post with a password.
  303. if ( ! empty( $this->args->ID ) && ! post_password_required( $this->args->ID ) ) {
  304. if ( $this->args->post_excerpt !== '' ) {
  305. $replacement = wp_strip_all_tags( $this->args->post_excerpt );
  306. }
  307. elseif ( $this->args->post_content !== '' ) {
  308. $content = strip_shortcodes( $this->args->post_content );
  309. $content = wp_strip_all_tags( $content );
  310. if ( strlen( utf8_decode( $content ) ) <= 156 ) {
  311. return $content;
  312. }
  313. $replacement = wp_html_excerpt( $content, 156 );
  314. // Trim the auto-generated string to a word boundary.
  315. $replacement = substr( $replacement, 0, strrpos( $replacement, ' ' ) );
  316. }
  317. }
  318. return $replacement;
  319. }
  320. /**
  321. * Retrieve the post/page/cpt excerpt for use as replacement string (without auto-generation).
  322. *
  323. * @return string|null
  324. */
  325. private function retrieve_excerpt_only() {
  326. $replacement = null;
  327. // The check `post_password_required` is because excerpt must be hidden for a post with a password.
  328. if ( ! empty( $this->args->ID ) && $this->args->post_excerpt !== '' && ! post_password_required( $this->args->ID ) ) {
  329. $replacement = wp_strip_all_tags( $this->args->post_excerpt );
  330. }
  331. return $replacement;
  332. }
  333. /**
  334. * Retrieve the title of the parent page of the current page/cpt for use as replacement string.
  335. * Only applicable for hierarchical post types.
  336. *
  337. * @todo Check: shouldn't this use $this->args as well ?
  338. *
  339. * @return string|null
  340. */
  341. private function retrieve_parent_title() {
  342. $replacement = null;
  343. if ( ! isset( $replacement ) && ( ( is_singular() || is_admin() ) && isset( $GLOBALS['post'] ) ) ) {
  344. if ( isset( $GLOBALS['post']->post_parent ) && 0 !== $GLOBALS['post']->post_parent ) {
  345. $replacement = get_the_title( $GLOBALS['post']->post_parent );
  346. }
  347. }
  348. return $replacement;
  349. }
  350. /**
  351. * Retrieve the current search phrase for use as replacement string.
  352. *
  353. * @return string|null
  354. */
  355. private function retrieve_searchphrase() {
  356. $replacement = null;
  357. if ( ! isset( $replacement ) ) {
  358. $search = get_query_var( 's' );
  359. if ( $search !== '' ) {
  360. $replacement = esc_html( $search );
  361. }
  362. }
  363. return $replacement;
  364. }
  365. /**
  366. * Retrieve the separator for use as replacement string.
  367. *
  368. * @return string
  369. */
  370. private function retrieve_sep() {
  371. return WPSEO_Utils::get_title_separator();
  372. }
  373. /**
  374. * Retrieve the site's tag line / description for use as replacement string.
  375. *
  376. * @return string|null
  377. */
  378. private function retrieve_sitedesc() {
  379. static $replacement;
  380. if ( ! isset( $replacement ) ) {
  381. $description = wp_strip_all_tags( get_bloginfo( 'description' ) );
  382. if ( $description !== '' ) {
  383. $replacement = $description;
  384. }
  385. }
  386. return $replacement;
  387. }
  388. /**
  389. * Retrieve the site's name for use as replacement string.
  390. *
  391. * @return string|null
  392. */
  393. private function retrieve_sitename() {
  394. static $replacement;
  395. if ( ! isset( $replacement ) ) {
  396. $sitename = WPSEO_Utils::get_site_name();
  397. if ( $sitename !== '' ) {
  398. $replacement = $sitename;
  399. }
  400. }
  401. return $replacement;
  402. }
  403. /**
  404. * Retrieve the current tag/tags for use as replacement string.
  405. *
  406. * @return string|null
  407. */
  408. private function retrieve_tag() {
  409. $replacement = null;
  410. if ( isset( $this->args->ID ) ) {
  411. $tags = $this->get_terms( $this->args->ID, 'post_tag' );
  412. if ( $tags !== '' ) {
  413. $replacement = $tags;
  414. }
  415. }
  416. return $replacement;
  417. }
  418. /**
  419. * Retrieve the tag description for use as replacement string.
  420. *
  421. * @return string|null
  422. */
  423. private function retrieve_tag_description() {
  424. return $this->retrieve_term_description();
  425. }
  426. /**
  427. * Retrieve the term description for use as replacement string.
  428. *
  429. * @return string|null
  430. */
  431. private function retrieve_term_description() {
  432. $replacement = null;
  433. if ( isset( $this->args->term_id ) && ! empty( $this->args->taxonomy ) ) {
  434. $term_desc = get_term_field( 'description', $this->args->term_id, $this->args->taxonomy );
  435. if ( $term_desc !== '' ) {
  436. $replacement = wp_strip_all_tags( $term_desc );
  437. }
  438. }
  439. return $replacement;
  440. }
  441. /**
  442. * Retrieve the term name for use as replacement string.
  443. *
  444. * @return string|null
  445. */
  446. private function retrieve_term_title() {
  447. $replacement = null;
  448. if ( ! empty( $this->args->taxonomy ) && ! empty( $this->args->name ) ) {
  449. $replacement = $this->args->name;
  450. }
  451. return $replacement;
  452. }
  453. /**
  454. * Retrieve the title of the post/page/cpt for use as replacement string.
  455. *
  456. * @return string|null
  457. */
  458. private function retrieve_title() {
  459. $replacement = null;
  460. if ( is_string( $this->args->post_title ) && $this->args->post_title !== '' ) {
  461. $replacement = stripslashes( $this->args->post_title );
  462. }
  463. return $replacement;
  464. }
  465. /**
  466. * Retrieve primary category for use as replacement string.
  467. *
  468. * @return bool|int|null
  469. */
  470. private function retrieve_primary_category() {
  471. $primary_category = null;
  472. if ( ! empty( $this->args->ID ) ) {
  473. $wpseo_primary_category = new WPSEO_Primary_Term( 'category', $this->args->ID );
  474. $term_id = $wpseo_primary_category->get_primary_term();
  475. $term = get_term( $term_id );
  476. if ( ! is_wp_error( $term ) && ! empty( $term ) ) {
  477. $primary_category = $term->name;
  478. }
  479. }
  480. return $primary_category;
  481. }
  482. /**
  483. * Retrieve the string generated by get_the_archive_title().
  484. *
  485. * @return string|null
  486. */
  487. private function retrieve_archive_title() {
  488. return get_the_archive_title();
  489. }
  490. /* *********************** ADVANCED VARIABLES ************************** */
  491. /**
  492. * Determine the page numbering of the current post/page/cpt.
  493. *
  494. * @param string $request Either 'nr'|'max' - whether to return the page number or the max number of pages.
  495. *
  496. * @return int|null
  497. */
  498. private function determine_pagenumbering( $request = 'nr' ) {
  499. global $wp_query, $post;
  500. $max_num_pages = null;
  501. $page_number = null;
  502. $max_num_pages = 1;
  503. if ( ! is_singular() ) {
  504. $page_number = get_query_var( 'paged' );
  505. if ( $page_number === 0 || $page_number === '' ) {
  506. $page_number = 1;
  507. }
  508. if ( ! empty( $wp_query->max_num_pages ) ) {
  509. $max_num_pages = $wp_query->max_num_pages;
  510. }
  511. }
  512. else {
  513. $page_number = get_query_var( 'page' );
  514. if ( $page_number === 0 || $page_number === '' ) {
  515. $page_number = 1;
  516. }
  517. if ( isset( $post->post_content ) ) {
  518. $max_num_pages = ( substr_count( $post->post_content, '<!--nextpage-->' ) + 1 );
  519. }
  520. }
  521. $return = null;
  522. switch ( $request ) {
  523. case 'nr':
  524. $return = $page_number;
  525. break;
  526. case 'max':
  527. $return = $max_num_pages;
  528. break;
  529. }
  530. return $return;
  531. }
  532. /**
  533. * Determine the post type names for the current post/page/cpt.
  534. *
  535. * @param string $request Either 'single'|'plural' - whether to return the single or plural form.
  536. *
  537. * @return string|null
  538. */
  539. private function determine_pt_names( $request = 'single' ) {
  540. global $wp_query;
  541. $pt_single = null;
  542. $pt_plural = null;
  543. $post_type = '';
  544. 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'] !== [] ) ) ) {
  545. $post_type = $wp_query->query_vars['post_type'];
  546. }
  547. elseif ( isset( $this->args->post_type ) && ( is_string( $this->args->post_type ) && $this->args->post_type !== '' ) ) {
  548. $post_type = $this->args->post_type;
  549. }
  550. else {
  551. // Make it work in preview mode.
  552. $post = $wp_query->get_queried_object();
  553. if ( $post instanceof WP_Post ) {
  554. $post_type = $post->post_type;
  555. }
  556. }
  557. if ( is_array( $post_type ) ) {
  558. $post_type = reset( $post_type );
  559. }
  560. if ( $post_type !== '' ) {
  561. $pt = get_post_type_object( $post_type );
  562. $pt_single = $pt->name;
  563. $pt_plural = $pt->name;
  564. if ( isset( $pt->labels->singular_name ) ) {
  565. $pt_single = $pt->labels->singular_name;
  566. }
  567. if ( isset( $pt->labels->name ) ) {
  568. $pt_plural = $pt->labels->name;
  569. }
  570. }
  571. $return = null;
  572. switch ( $request ) {
  573. case 'single':
  574. $return = $pt_single;
  575. break;
  576. case 'plural':
  577. $return = $pt_plural;
  578. break;
  579. }
  580. return $return;
  581. }
  582. /**
  583. * Retrieve the attachment caption for use as replacement string.
  584. *
  585. * @return string|null
  586. */
  587. private function retrieve_caption() {
  588. return $this->retrieve_excerpt_only();
  589. }
  590. /**
  591. * Retrieve a post/page/cpt's custom field value for use as replacement string.
  592. *
  593. * @param string $var The complete variable to replace which includes the name of
  594. * the custom field which value is to be retrieved.
  595. *
  596. * @return string|null
  597. */
  598. private function retrieve_cf_custom_field_name( $var ) {
  599. global $post;
  600. $replacement = null;
  601. if ( is_string( $var ) && $var !== '' ) {
  602. $field = substr( $var, 3 );
  603. if ( ( is_singular() || is_admin() ) && ( is_object( $post ) && isset( $post->ID ) ) ) {
  604. $name = get_post_meta( $post->ID, $field, true );
  605. if ( $name !== '' ) {
  606. $replacement = $name;
  607. }
  608. }
  609. }
  610. return $replacement;
  611. }
  612. /**
  613. * Retrieve a post/page/cpt's custom taxonomies for use as replacement string.
  614. *
  615. * @param string $var The complete variable to replace which includes the name of
  616. * the custom taxonomy which value(s) is to be retrieved.
  617. * @param bool $single Whether to retrieve only the first or all values for the taxonomy.
  618. *
  619. * @return string|null
  620. */
  621. private function retrieve_ct_custom_tax_name( $var, $single = false ) {
  622. $replacement = null;
  623. if ( ( is_string( $var ) && $var !== '' ) && ! empty( $this->args->ID ) ) {
  624. $tax = substr( $var, 3 );
  625. $name = $this->get_terms( $this->args->ID, $tax, $single );
  626. if ( $name !== '' ) {
  627. $replacement = $name;
  628. }
  629. }
  630. return $replacement;
  631. }
  632. /**
  633. * Retrieve a post/page/cpt's custom taxonomies description for use as replacement string.
  634. *
  635. * @param string $var The complete variable to replace which includes the name of
  636. * the custom taxonomy which description is to be retrieved.
  637. *
  638. * @return string|null
  639. */
  640. private function retrieve_ct_desc_custom_tax_name( $var ) {
  641. global $post;
  642. $replacement = null;
  643. if ( is_string( $var ) && $var !== '' ) {
  644. $tax = substr( $var, 8 );
  645. if ( is_object( $post ) && isset( $post->ID ) ) {
  646. $terms = get_the_terms( $post->ID, $tax );
  647. if ( is_array( $terms ) && $terms !== [] ) {
  648. $term = current( $terms );
  649. $term_desc = get_term_field( 'description', $term->term_id, $tax );
  650. if ( $term_desc !== '' ) {
  651. $replacement = wp_strip_all_tags( $term_desc );
  652. }
  653. }
  654. }
  655. }
  656. return $replacement;
  657. }
  658. /**
  659. * Retrieve the current date for use as replacement string.
  660. *
  661. * @return string The formatted current date.
  662. */
  663. private function retrieve_currentdate() {
  664. static $replacement;
  665. if ( ! isset( $replacement ) ) {
  666. $replacement = date_i18n( get_option( 'date_format' ) );
  667. }
  668. return $replacement;
  669. }
  670. /**
  671. * Retrieve the current day for use as replacement string.
  672. *
  673. * @return string The current day.
  674. */
  675. private function retrieve_currentday() {
  676. static $replacement;
  677. if ( ! isset( $replacement ) ) {
  678. $replacement = date_i18n( 'j' );
  679. }
  680. return $replacement;
  681. }
  682. /**
  683. * Retrieve the current month for use as replacement string.
  684. *
  685. * @return string The current month.
  686. */
  687. private function retrieve_currentmonth() {
  688. static $replacement;
  689. if ( ! isset( $replacement ) ) {
  690. $replacement = date_i18n( 'F' );
  691. }
  692. return $replacement;
  693. }
  694. /**
  695. * Retrieve the current time for use as replacement string.
  696. *
  697. * @return string The formatted current time.
  698. */
  699. private function retrieve_currenttime() {
  700. static $replacement;
  701. if ( ! isset( $replacement ) ) {
  702. $replacement = date_i18n( get_option( 'time_format' ) );
  703. }
  704. return $replacement;
  705. }
  706. /**
  707. * Retrieve the current year for use as replacement string.
  708. *
  709. * @return string The current year.
  710. */
  711. private function retrieve_currentyear() {
  712. static $replacement;
  713. if ( ! isset( $replacement ) ) {
  714. $replacement = date_i18n( 'Y' );
  715. }
  716. return $replacement;
  717. }
  718. /**
  719. * Retrieve the post/page/cpt's focus keyword for use as replacement string.
  720. *
  721. * @return string|null
  722. */
  723. private function retrieve_focuskw() {
  724. // Retrieve focuskw from a Post.
  725. if ( ! empty( $this->args->ID ) ) {
  726. $focus_kw = WPSEO_Meta::get_value( 'focuskw', $this->args->ID );
  727. if ( $focus_kw !== '' ) {
  728. return $focus_kw;
  729. }
  730. return null;
  731. }
  732. // Retrieve focuskw from a Term.
  733. if ( ! empty( $this->args->term_id ) ) {
  734. $focus_kw = WPSEO_Taxonomy_Meta::get_term_meta( $this->args->term_id, $this->args->taxonomy, 'focuskw' );
  735. if ( $focus_kw !== '' ) {
  736. return $focus_kw;
  737. }
  738. }
  739. return null;
  740. }
  741. /**
  742. * Retrieve the post/page/cpt ID for use as replacement string.
  743. *
  744. * @return string|null
  745. */
  746. private function retrieve_id() {
  747. $replacement = null;
  748. if ( ! empty( $this->args->ID ) ) {
  749. $replacement = $this->args->ID;
  750. }
  751. return $replacement;
  752. }
  753. /**
  754. * Retrieve the post/page/cpt modified time for use as replacement string.
  755. *
  756. * @return string|null
  757. */
  758. private function retrieve_modified() {
  759. $replacement = null;
  760. if ( ! empty( $this->args->post_modified ) ) {
  761. $replacement = $this->date->format_translated( $this->args->post_modified, get_option( 'date_format' ) );
  762. }
  763. return $replacement;
  764. }
  765. /**
  766. * Retrieve the post/page/cpt author's "nice name" for use as replacement string.
  767. *
  768. * @return string|null
  769. */
  770. private function retrieve_name() {
  771. $replacement = null;
  772. $user_id = $this->retrieve_userid();
  773. $name = get_the_author_meta( 'display_name', $user_id );
  774. if ( $name !== '' ) {
  775. $replacement = $name;
  776. }
  777. return $replacement;
  778. }
  779. /**
  780. * Retrieve the post/page/cpt author's users description for use as a replacement string.
  781. *
  782. * @return null|string
  783. */
  784. private function retrieve_user_description() {
  785. $replacement = null;
  786. $user_id = $this->retrieve_userid();
  787. $description = get_the_author_meta( 'description', $user_id );
  788. if ( $description !== '' ) {
  789. $replacement = $description;
  790. }
  791. return $replacement;
  792. }
  793. /**
  794. * Retrieve the current page number with context (i.e. 'page 2 of 4') for use as replacement string.
  795. *
  796. * @return string
  797. */
  798. private function retrieve_page() {
  799. $replacement = null;
  800. $max = $this->determine_pagenumbering( 'max' );
  801. $nr = $this->determine_pagenumbering( 'nr' );
  802. $sep = $this->retrieve_sep();
  803. if ( $max > 1 && $nr > 1 ) {
  804. /* translators: 1: current page number, 2: total number of pages. */
  805. $replacement = sprintf( $sep . ' ' . __( 'Page %1$d of %2$d', 'wordpress-seo' ), $nr, $max );
  806. }
  807. return $replacement;
  808. }
  809. /**
  810. * Retrieve the current page number for use as replacement string.
  811. *
  812. * @return string|null
  813. */
  814. private function retrieve_pagenumber() {
  815. $replacement = null;
  816. $nr = $this->determine_pagenumbering( 'nr' );
  817. if ( isset( $nr ) && $nr > 0 ) {
  818. $replacement = (string) $nr;
  819. }
  820. return $replacement;
  821. }
  822. /**
  823. * Retrieve the current page total for use as replacement string.
  824. *
  825. * @return string|null
  826. */
  827. private function retrieve_pagetotal() {
  828. $replacement = null;
  829. $max = $this->determine_pagenumbering( 'max' );
  830. if ( isset( $max ) && $max > 0 ) {
  831. $replacement = (string) $max;
  832. }
  833. return $replacement;
  834. }
  835. /**
  836. * Retrieve the post type plural label for use as replacement string.
  837. *
  838. * @return string|null
  839. */
  840. private function retrieve_pt_plural() {
  841. $replacement = null;
  842. $name = $this->determine_pt_names( 'plural' );
  843. if ( isset( $name ) && $name !== '' ) {
  844. $replacement = $name;
  845. }
  846. return $replacement;
  847. }
  848. /**
  849. * Retrieve the post type single label for use as replacement string.
  850. *
  851. * @return string|null
  852. */
  853. private function retrieve_pt_single() {
  854. $replacement = null;
  855. $name = $this->determine_pt_names( 'single' );
  856. if ( isset( $name ) && $name !== '' ) {
  857. $replacement = $name;
  858. }
  859. return $replacement;
  860. }
  861. /**
  862. * Retrieve the slug which caused the 404 for use as replacement string.
  863. *
  864. * @return string|null
  865. */
  866. private function retrieve_term404() {
  867. $replacement = null;
  868. if ( $this->args->term404 !== '' ) {
  869. $replacement = sanitize_text_field( str_replace( '-', ' ', $this->args->term404 ) );
  870. }
  871. else {
  872. $error_request = get_query_var( 'pagename' );
  873. if ( $error_request !== '' ) {
  874. $replacement = sanitize_text_field( str_replace( '-', ' ', $error_request ) );
  875. }
  876. else {
  877. $error_request = get_query_var( 'name' );
  878. if ( $error_request !== '' ) {
  879. $replacement = sanitize_text_field( str_replace( '-', ' ', $error_request ) );
  880. }
  881. }
  882. }
  883. return $replacement;
  884. }
  885. /**
  886. * Retrieve the post/page/cpt author's user id for use as replacement string.
  887. *
  888. * @return string
  889. */
  890. private function retrieve_userid() {
  891. $replacement = ! empty( $this->args->post_author ) ? $this->args->post_author : get_query_var( 'author' );
  892. return $replacement;
  893. }
  894. /* *********************** HELP TEXT RELATED ************************** */
  895. /**
  896. * Set the help text for a user/plugin/theme defined extra variable.
  897. *
  898. * @param string $type Type of variable: 'basic' or 'advanced'.
  899. * @param WPSEO_Replacement_Variable $replacement_variable The replacement variable to register.
  900. */
  901. private static function register_help_text( $type, WPSEO_Replacement_Variable $replacement_variable ) {
  902. $identifier = $replacement_variable->get_variable();
  903. if ( ( is_string( $type ) && in_array( $type, [ 'basic', 'advanced' ], true ) )
  904. && ( $identifier !== '' && ! isset( self::$help_texts[ $type ][ $identifier ] ) )
  905. ) {
  906. self::$help_texts[ $type ][ $identifier ] = $replacement_variable;
  907. }
  908. }
  909. /**
  910. * Generates a list of replacement variables based on the help texts.
  911. *
  912. * @return array List of replace vars.
  913. */
  914. public function get_replacement_variables_list() {
  915. self::setup_statics_once();
  916. $replacement_variables = array_merge(
  917. $this->get_replacement_variables(),
  918. WPSEO_Custom_Fields::get_custom_fields(),
  919. WPSEO_Custom_Taxonomies::get_custom_taxonomies()
  920. );
  921. return array_map( [ $this, 'format_replacement_variable' ], $replacement_variables );
  922. }
  923. /**
  924. * Creates a merged associative array of both the basic and advanced help texts.
  925. *
  926. * @return array Array with the replacement variables.
  927. */
  928. private function get_replacement_variables() {
  929. $help_texts = array_merge( self::$help_texts['basic'], self::$help_texts['advanced'] );
  930. return array_filter( array_keys( $help_texts ), [ $this, 'is_not_prefixed' ] );
  931. }
  932. /**
  933. * Checks whether the replacement variable contains a `ct_` or `cf_` prefix, because they follow different logic.
  934. *
  935. * @param string $replacement_variable The replacement variable.
  936. *
  937. * @return bool True when the replacement variable is not prefixed.
  938. */
  939. private function is_not_prefixed( $replacement_variable ) {
  940. $prefixes = [ 'cf_', 'ct_' ];
  941. $prefix = $this->get_prefix( $replacement_variable );
  942. return ! in_array( $prefix, $prefixes, true );
  943. }
  944. /**
  945. * Strip the prefix from a replacement variable name.
  946. *
  947. * @param string $replacement_variable The replacement variable.
  948. *
  949. * @return string The replacement variable name without the prefix.
  950. */
  951. private function strip_prefix( $replacement_variable ) {
  952. return substr( $replacement_variable, 3 );
  953. }
  954. /**
  955. * Gets the prefix from a replacement variable name.
  956. *
  957. * @param string $replacement_variable The replacement variable.
  958. *
  959. * @return string The prefix of the replacement variable.
  960. */
  961. private function get_prefix( $replacement_variable ) {
  962. return substr( $replacement_variable, 0, 3 );
  963. }
  964. /**
  965. * Strips 'desc_' if present, and appends ' description' at the end.
  966. *
  967. * @param string $label The replacement variable.
  968. *
  969. * @return string The altered replacement variable name.
  970. */
  971. private function handle_description( $label ) {
  972. if ( strpos( $label, 'desc_' ) === 0 ) {
  973. return substr( $label, 5 ) . ' description';
  974. }
  975. return $label;
  976. }
  977. /**
  978. * Creates a label for prefixed replacement variables that matches the format in the editors.
  979. *
  980. * @param string $replacement_variable The replacement variable.
  981. *
  982. * @return string The replacement variable label.
  983. */
  984. private function get_label( $replacement_variable ) {
  985. $prefix = $this->get_prefix( $replacement_variable );
  986. if ( $prefix === 'cf_' ) {
  987. return $this->strip_prefix( $replacement_variable ) . ' (custom field)';
  988. }
  989. if ( $prefix === 'ct_' ) {
  990. $label = $this->strip_prefix( $replacement_variable );
  991. $label = $this->handle_description( $label );
  992. return ucfirst( $label . ' (custom taxonomy)' );
  993. }
  994. if ( $prefix === 'pt_' ) {
  995. if ( $replacement_variable === 'pt_single' ) {
  996. return 'Post type (singular)';
  997. }
  998. return 'Post type (plural)';
  999. }
  1000. return '';
  1001. }
  1002. /**
  1003. * Formats the replacement variables.
  1004. *
  1005. * @param string $replacement_variable The replacement variable to format.
  1006. *
  1007. * @return array The formatted replacement variable.
  1008. */
  1009. private function format_replacement_variable( $replacement_variable ) {
  1010. return [
  1011. 'name' => $replacement_variable,
  1012. 'value' => '',
  1013. 'label' => $this->get_label( $replacement_variable ),
  1014. ];
  1015. }
  1016. /**
  1017. * Retrieves the custom field names as an array.
  1018. *
  1019. * @link WordPress core: wp-admin/includes/template.php. Reused query from it.
  1020. *
  1021. * @return array The custom fields.
  1022. */
  1023. private function get_custom_fields() {
  1024. global $wpdb;
  1025. /**
  1026. * Filters the number of custom fields to retrieve for the drop-down
  1027. * in the Custom Fields meta box.
  1028. *
  1029. * @since 2.1.0
  1030. *
  1031. * @param int $limit Number of custom fields to retrieve. Default 30.
  1032. */
  1033. $limit = apply_filters( 'postmeta_form_limit', 30 );
  1034. $sql = "SELECT DISTINCT meta_key
  1035. FROM $wpdb->postmeta
  1036. WHERE meta_key NOT BETWEEN '_' AND '_z'
  1037. HAVING meta_key NOT LIKE %s
  1038. ORDER BY meta_key
  1039. LIMIT %d";
  1040. $fields = $wpdb->get_col( $wpdb->prepare( $sql, $wpdb->esc_like( '_' ) . '%', $limit ) );
  1041. if ( is_array( $fields ) ) {
  1042. return array_map( [ $this, 'add_custom_field_prefix' ], $fields );
  1043. }
  1044. return [];
  1045. }
  1046. /**
  1047. * Adds the cf_ prefix to a field.
  1048. *
  1049. * @param string $field The field to prefix.
  1050. *
  1051. * @return string The prefixed field.
  1052. */
  1053. private function add_custom_field_prefix( $field ) {
  1054. return 'cf_' . $field;
  1055. }
  1056. /**
  1057. * Gets the names of the custom taxonomies, prepends 'ct_' and 'ct_desc', and returns them in an array.
  1058. *
  1059. * @return array The custom taxonomy prefixed names.
  1060. */
  1061. private function get_custom_taxonomies() {
  1062. $args = [
  1063. 'public' => true,
  1064. '_builtin' => false,
  1065. ];
  1066. $output = 'names';
  1067. $operator = 'and';
  1068. $custom_taxonomies = get_taxonomies( $args, $output, $operator );
  1069. if ( is_array( $custom_taxonomies ) ) {
  1070. $ct_replace_vars = [];
  1071. foreach ( $custom_taxonomies as $custom_taxonomy ) {
  1072. array_push( $ct_replace_vars, 'ct_' . $custom_taxonomy, 'ct_desc_' . $custom_taxonomy );
  1073. }
  1074. return $ct_replace_vars;
  1075. }
  1076. return [];
  1077. }
  1078. /**
  1079. * Set/translate the help texts for the WPSEO standard basic variables.
  1080. */
  1081. private static function set_basic_help_texts() {
  1082. /* translators: %s: wp_title() function. */
  1083. $separator_description = __( 'The separator defined in your theme\'s %s tag.', 'wordpress-seo' );
  1084. $separator_description = sprintf(
  1085. $separator_description,
  1086. // '<code>wp_title()</code>'
  1087. 'wp_title()'
  1088. );
  1089. $replacement_variables = [
  1090. new WPSEO_Replacement_Variable( 'date', __( 'Date', 'wordpress-seo' ), __( 'Replaced with the date of the post/page', 'wordpress-seo' ) ),
  1091. new WPSEO_Replacement_Variable( 'title', __( 'Title', 'wordpress-seo' ), __( 'Replaced with the title of the post/page', 'wordpress-seo' ) ),
  1092. new WPSEO_Replacement_Variable( 'parent_title', __( 'Parent title', 'wordpress-seo' ), __( 'Replaced with the title of the parent page of the current page', 'wordpress-seo' ) ),
  1093. new WPSEO_Replacement_Variable( 'archive_title', __( 'Archive title', 'wordpress-seo' ), __( 'Replaced with the normal title for an archive generated by WordPress', 'wordpress-seo' ) ),
  1094. new WPSEO_Replacement_Variable( 'sitename', __( 'Site title', 'wordpress-seo' ), __( 'The site\'s name', 'wordpress-seo' ) ),
  1095. new WPSEO_Replacement_Variable( 'sitedesc', __( 'Tagline', 'wordpress-seo' ), __( 'The site\'s tagline', 'wordpress-seo' ) ),
  1096. new WPSEO_Replacement_Variable( 'excerpt', __( 'Excerpt', 'wordpress-seo' ), __( 'Replaced with the post/page excerpt (or auto-generated if it does not exist)', 'wordpress-seo' ) ),
  1097. new WPSEO_Replacement_Variable( 'excerpt_only', __( 'Excerpt only', 'wordpress-seo' ), __( 'Replaced with the post/page excerpt (without auto-generation)', 'wordpress-seo' ) ),
  1098. new WPSEO_Replacement_Variable( 'tag', __( 'Tag', 'wordpress-seo' ), __( 'Replaced with the current tag/tags', 'wordpress-seo' ) ),
  1099. new WPSEO_Replacement_Variable( 'category', __( 'Category', 'wordpress-seo' ), __( 'Replaced with the post categories (comma separated)', 'wordpress-seo' ) ),
  1100. new WPSEO_Replacement_Variable( 'primary_category', __( 'Primary category', 'wordpress-seo' ), __( 'Replaced with the primary category of the post/page', 'wordpress-seo' ) ),
  1101. new WPSEO_Replacement_Variable( 'category_description', __( 'Category description', 'wordpress-seo' ), __( 'Replaced with the category description', 'wordpress-seo' ) ),
  1102. new WPSEO_Replacement_Variable( 'tag_description', __( 'Tag description', 'wordpress-seo' ), __( 'Replaced with the tag description', 'wordpress-seo' ) ),
  1103. new WPSEO_Replacement_Variable( 'term_description', __( 'Term description', 'wordpress-seo' ), __( 'Replaced with the term description', 'wordpress-seo' ) ),
  1104. new WPSEO_Replacement_Variable( 'term_title', __( 'Term title', 'wordpress-seo' ), __( 'Replaced with the term name', 'wordpress-seo' ) ),
  1105. new WPSEO_Replacement_Variable( 'searchphrase', __( 'Search phrase', 'wordpress-seo' ), __( 'Replaced with the current search phrase', 'wordpress-seo' ) ),
  1106. new WPSEO_Replacement_Variable( 'sep', __( 'Separator', 'wordpress-seo' ), $separator_description ),
  1107. ];
  1108. foreach ( $replacement_variables as $replacement_variable ) {
  1109. self::register_help_text( 'basic', $replacement_variable );
  1110. }
  1111. }
  1112. /**
  1113. * Set/translate the help texts for the WPSEO standard advanced variables.
  1114. */
  1115. private static function set_advanced_help_texts() {
  1116. $replacement_variables = [
  1117. new WPSEO_Replacement_Variable( 'pt_single', __( 'Post type (singular)', 'wordpress-seo' ), __( 'Replaced with the content type single label', 'wordpress-seo' ) ),
  1118. new WPSEO_Replacement_Variable( 'pt_plural', __( 'Post type (plural)', 'wordpress-seo' ), __( 'Replaced with the content type plural label', 'wordpress-seo' ) ),
  1119. new WPSEO_Replacement_Variable( 'modified', __( 'Modified', 'wordpress-seo' ), __( 'Replaced with the post/page modified time', 'wordpress-seo' ) ),
  1120. new WPSEO_Replacement_Variable( 'id', __( 'ID', 'wordpress-seo' ), __( 'Replaced with the post/page ID', 'wordpress-seo' ) ),
  1121. new WPSEO_Replacement_Variable( 'name', __( 'Name', 'wordpress-seo' ), __( 'Replaced with the post/page author\'s \'nicename\'', 'wordpress-seo' ) ),
  1122. new WPSEO_Replacement_Variable( 'user_description', __( 'User description', 'wordpress-seo' ), __( 'Replaced with the post/page author\'s \'Biographical Info\'', 'wordpress-seo' ) ),
  1123. new WPSEO_Replacement_Variable( 'page', __( 'Page number', 'wordpress-seo' ), __( 'Replaced with the current page number with context (i.e. page 2 of 4)', 'wordpress-seo' ) ),
  1124. new WPSEO_Replacement_Variable( 'pagetotal', __( 'Pagetotal', 'wordpress-seo' ), __( 'Replaced with the current page total', 'wordpress-seo' ) ),
  1125. new WPSEO_Replacement_Variable( 'pagenumber', __( 'Pagenumber', 'wordpress-seo' ), __( 'Replaced with the current page number', 'wordpress-seo' ) ),
  1126. new WPSEO_Replacement_Variable( 'caption', __( 'Caption', 'wordpress-seo' ), __( 'Attachment caption', 'wordpress-seo' ) ),
  1127. new WPSEO_Replacement_Variable( 'focuskw', __( 'Focus keyword', 'wordpress-seo' ), __( 'Replaced with the posts focus keyphrase', 'wordpress-seo' ) ),
  1128. new WPSEO_Replacement_Variable( 'term404', __( 'Term404', 'wordpress-seo' ), __( 'Replaced with the slug which caused the 404', 'wordpress-seo' ) ),
  1129. new WPSEO_Replacement_Variable( 'cf_<custom-field-name>', '<custom-field-name> ' . __( '(custom field)', 'wordpress-seo' ), __( 'Replaced with a posts custom field value', 'wordpress-seo' ) ),
  1130. 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' ) ),
  1131. 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' ) ),
  1132. ];
  1133. foreach ( $replacement_variables as $replacement_variable ) {
  1134. self::register_help_text( 'advanced', $replacement_variable );
  1135. }
  1136. }
  1137. /* *********************** GENERAL HELPER METHODS ************************** */
  1138. /**
  1139. * Remove the '%%' delimiters from a variable string.
  1140. *
  1141. * @param string $string Variable string to be cleaned.
  1142. *
  1143. * @return string
  1144. */
  1145. private static function remove_var_delimiter( $string ) {
  1146. return trim( $string, '%' );
  1147. }
  1148. /**
  1149. * Add the '%%' delimiters to a variable string.
  1150. *
  1151. * @param string $string Variable string to be delimited.
  1152. *
  1153. * @return string
  1154. */
  1155. private static function add_var_delimiter( $string ) {
  1156. return '%%' . $string . '%%';
  1157. }
  1158. /**
  1159. * Retrieve a post's terms, comma delimited.
  1160. *
  1161. * @param int $id ID of the post to get the terms for.
  1162. * @param string $taxonomy The taxonomy to get the terms for this post from.
  1163. * @param bool $return_single If true, return the first term.
  1164. *
  1165. * @return string Either a single term or a comma delimited string of terms.
  1166. */
  1167. public function get_terms( $id, $taxonomy, $return_single = false ) {
  1168. $output = '';
  1169. // If we're on a specific tag, category or taxonomy page, use that.
  1170. if ( is_category() || is_tag() || is_tax() ) {
  1171. $term = $GLOBALS['wp_query']->get_queried_object();
  1172. $output = $term->name;
  1173. }
  1174. elseif ( ! empty( $id ) && ! empty( $taxonomy ) ) {
  1175. $terms = get_the_terms( $id, $taxonomy );
  1176. if ( is_array( $terms ) && $terms !== [] ) {
  1177. foreach ( $terms as $term ) {
  1178. if ( $return_single ) {
  1179. $output = $term->name;
  1180. break;
  1181. }
  1182. else {
  1183. $output .= $term->name . ', ';
  1184. }
  1185. }
  1186. $output = rtrim( trim( $output ), ',' );
  1187. }
  1188. }
  1189. unset( $terms, $term );
  1190. /**
  1191. * Allows filtering of the terms list used to replace %%category%%, %%tag%%
  1192. * and %%ct_<custom-tax-name>%% variables.
  1193. *
  1194. * @api string $output Comma-delimited string containing the terms.
  1195. * @api string $taxonomy The taxonomy of the terms.
  1196. */
  1197. return apply_filters( 'wpseo_terms', $output, $taxonomy );
  1198. }
  1199. } /* End of class WPSEO_Replace_Vars */