class-wpseo-taxonomy-meta.php 19 KB

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