class-wpseo-taxonomy-meta.php 18 KB

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