class-wpseo-option.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709
  1. <?php
  2. /**
  3. * WPSEO plugin file.
  4. *
  5. * @package WPSEO\Internals\Options
  6. */
  7. /**
  8. * This abstract class and it's concrete classes implement defaults and value validation for
  9. * all WPSEO options and subkeys within options.
  10. *
  11. * Some guidelines:
  12. * [Retrieving options]
  13. * - Use the normal get_option() to retrieve an option. You will receive a complete array for the option.
  14. * Any subkeys which were not set, will have their default values in place.
  15. * - In other words, you will normally not have to check whether a subkey isset() as they will *always* be set.
  16. * They will also *always* be of the correct variable type.
  17. * The only exception to this are the options with variable option names based on post_type or taxonomy
  18. * as those will not always be available before the taxonomy/post_type is registered.
  19. * (they will be available if a value was set, they won't be if it wasn't as the class won't know
  20. * that a default needs to be injected).
  21. *
  22. * [Updating/Adding options]
  23. * - For multisite site_options, please use the WPSEO_Options::update_site_option() method.
  24. * - For normal options, use the normal add/update_option() functions. As long a the classes here
  25. * are instantiated, validation for all options and their subkeys will be automatic.
  26. * - On (succesfull) update of a couple of options, certain related actions will be run automatically.
  27. * Some examples:
  28. * - on change of wpseo[yoast_tracking], the cron schedule will be adjusted accordingly
  29. * - on change of wpseo and wpseo_title, some caches will be cleared
  30. *
  31. *
  32. * [Important information about add/updating/changing these classes]
  33. * - Make sure that option array key names are unique across options. The WPSEO_Options::get_all()
  34. * method merges most options together. If any of them have non-unique names, even if they
  35. * are in a different option, they *will* overwrite each other.
  36. * - When you add a new array key in an option: make sure you add proper defaults and add the key
  37. * to the validation routine in the proper place or add a new validation case.
  38. * You don't need to do any upgrading as any option returned will always be merged with the
  39. * defaults, so new options will automatically be available.
  40. * If the default value is a string which need translating, add this to the concrete class
  41. * translate_defaults() method.
  42. * - When you remove an array key from an option: if it's important that the option is really removed,
  43. * add the WPSEO_Option::clean_up( $option_name ) method to the upgrade run.
  44. * This will re-save the option and automatically remove the array key no longer in existance.
  45. * - When you rename a sub-option: add it to the clean_option() routine and run that in the upgrade run.
  46. * - When you change the default for an option sub-key, make sure you verify that the validation routine will
  47. * still work the way it should.
  48. * Example: changing a default from '' (empty string) to 'text' with a validation routine with tests
  49. * for an empty string will prevent a user from saving an empty string as the real value. So the
  50. * test for '' with the validation routine would have to be removed in that case.
  51. * - If an option needs specific actions different from defined in this abstract class, you can just overrule
  52. * a method by defining it in the concrete class.
  53. *
  54. * @todo - [JRF => testers] Double check that validation will not cause errors when called
  55. * from upgrade routine (some of the WP functions may not yet be available).
  56. */
  57. abstract class WPSEO_Option {
  58. /**
  59. * @var string Option name - MUST be set in concrete class and set to public.
  60. */
  61. protected $option_name;
  62. /**
  63. * @var string Option group name for use in settings forms
  64. * - will be set automagically if not set in concrete class
  65. * (i.e. if it confirm to the normal pattern 'yoast' . $option_name . 'options',
  66. * only set in conrete class if it doesn't)
  67. */
  68. public $group_name;
  69. /**
  70. * @var bool Whether to include the option in the return for WPSEO_Options::get_all().
  71. * Also determines which options are copied over for ms_(re)set_blog().
  72. */
  73. public $include_in_all = true;
  74. /**
  75. * @var bool Whether this option is only for when the install is multisite.
  76. */
  77. public $multisite_only = false;
  78. /**
  79. * @var array Array of defaults for the option - MUST be set in concrete class.
  80. * Shouldn't be requested directly, use $this->get_defaults();
  81. */
  82. protected $defaults;
  83. /**
  84. * @var array Array of variable option name patterns for the option - if any -
  85. * Set this when the option contains array keys which vary based on post_type
  86. * or taxonomy.
  87. */
  88. protected $variable_array_key_patterns;
  89. /**
  90. * @var array Array of sub-options which should not be overloaded with multi-site defaults.
  91. */
  92. public $ms_exclude = array();
  93. /**
  94. * @var object Instance of this class.
  95. */
  96. protected static $instance;
  97. /* *********** INSTANTIATION METHODS *********** */
  98. /**
  99. * Add all the actions and filters for the option.
  100. *
  101. * @return \WPSEO_Option
  102. */
  103. protected function __construct() {
  104. /* Add filters which get applied to the get_options() results. */
  105. $this->add_default_filters(); // Return defaults if option not set.
  106. $this->add_option_filters(); // Merge with defaults if option *is* set.
  107. if ( $this->multisite_only !== true ) {
  108. /**
  109. * The option validation routines remove the default filters to prevent failing
  110. * to insert an option if it's new. Let's add them back afterwards.
  111. */
  112. add_action( 'add_option', array( $this, 'add_default_filters' ) ); // Adding back after INSERT.
  113. add_action( 'update_option', array( $this, 'add_default_filters' ) );
  114. }
  115. elseif ( is_multisite() ) {
  116. /*
  117. * The option validation routines remove the default filters to prevent failing
  118. * to insert an option if it's new. Let's add them back afterwards.
  119. *
  120. * For site_options, this method is not foolproof as these actions are not fired
  121. * on an insert/update failure. Please use the WPSEO_Options::update_site_option() method
  122. * for updating site options to make sure the filters are in place.
  123. */
  124. add_action( 'add_site_option_' . $this->option_name, array( $this, 'add_default_filters' ) );
  125. add_action( 'update_site_option_' . $this->option_name, array( $this, 'add_default_filters' ) );
  126. }
  127. /*
  128. * Make sure the option will always get validated, independently of register_setting()
  129. * (only available on back-end).
  130. */
  131. add_filter( 'sanitize_option_' . $this->option_name, array( $this, 'validate' ) );
  132. // Flushes the rewrite rules when option is updated.
  133. add_action( 'update_option_' . $this->option_name, array( 'WPSEO_Utils', 'clear_rewrites' ) );
  134. /* Register our option for the admin pages */
  135. add_action( 'admin_init', array( $this, 'register_setting' ) );
  136. /* Set option group name if not given */
  137. if ( ! isset( $this->group_name ) || $this->group_name === '' ) {
  138. $this->group_name = 'yoast_' . $this->option_name . '_options';
  139. }
  140. /* Translate some defaults as early as possible - textdomain is loaded in init on priority 1. */
  141. if ( method_exists( $this, 'translate_defaults' ) ) {
  142. add_action( 'init', array( $this, 'translate_defaults' ), 2 );
  143. }
  144. /**
  145. * Enrich defaults once custom post types and taxonomies have been registered
  146. * which is normally done on the init action.
  147. *
  148. * @todo - [JRF/testers] Verify that none of the options which are only available after
  149. * enrichment are used before the enriching.
  150. */
  151. if ( method_exists( $this, 'enrich_defaults' ) ) {
  152. add_action( 'init', array( $this, 'enrich_defaults' ), 99 );
  153. }
  154. }
  155. // @codingStandardsIgnoreStart
  156. /**
  157. * All concrete classes *must* contain the get_instance method.
  158. *
  159. * {@internal Unfortunately I can't define it as an abstract as it also *has* to be static...}}
  160. */
  161. // abstract protected static function get_instance();
  162. /**
  163. * Concrete classes *may* contain a translate_defaults method.
  164. */
  165. // abstract public function translate_defaults();
  166. /**
  167. * Concrete classes *may* contain a enrich_defaults method to add additional defaults once
  168. * all post_types and taxonomies have been registered.
  169. */
  170. // abstract public function enrich_defaults();
  171. /* *********** METHODS INFLUENCING get_option() *********** */
  172. /**
  173. * Add filters to make sure that the option default is returned if the option is not set.
  174. *
  175. * @return void
  176. */
  177. public function add_default_filters() {
  178. // Don't change, needs to check for false as could return prio 0 which would evaluate to false.
  179. if ( has_filter( 'default_option_' . $this->option_name, array( $this, 'get_defaults' ) ) === false ) {
  180. add_filter( 'default_option_' . $this->option_name, array( $this, 'get_defaults' ) );
  181. }
  182. }
  183. // @codingStandardsIgnoreStart
  184. /**
  185. * Validate webmaster tools & Pinterest verification strings.
  186. *
  187. * @param string $key Key to check, by type of service.
  188. * @param array $dirty Dirty data.
  189. * @param array $old Old data.
  190. * @param array $clean Clean data by reference.
  191. */
  192. public function validate_verification_string( $key, $dirty, $old, &$clean ) {
  193. if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
  194. $meta = $dirty[ $key ];
  195. if ( strpos( $meta, 'content=' ) ) {
  196. // Make sure we only have the real key, not a complete meta tag.
  197. preg_match( '`content=([\'"])?([^\'"> ]+)(?:\1|[ />])`', $meta, $match );
  198. if ( isset( $match[2] ) ) {
  199. $meta = $match[2];
  200. }
  201. unset( $match );
  202. }
  203. $meta = sanitize_text_field( $meta );
  204. if ( $meta !== '' ) {
  205. $regex = '`^[A-Fa-f0-9_-]+$`';
  206. $service = '';
  207. switch ( $key ) {
  208. case 'baiduverify':
  209. $regex = '`^[A-Za-z0-9_-]+$`';
  210. $service = 'Baidu Webmaster tools';
  211. break;
  212. case 'googleverify':
  213. $regex = '`^[A-Za-z0-9_-]+$`';
  214. $service = 'Google Webmaster tools';
  215. break;
  216. case 'msverify':
  217. $service = 'Bing Webmaster tools';
  218. break;
  219. case 'pinterestverify':
  220. $service = 'Pinterest';
  221. break;
  222. case 'yandexverify':
  223. $service = 'Yandex Webmaster tools';
  224. break;
  225. }
  226. if ( preg_match( $regex, $meta ) ) {
  227. $clean[ $key ] = $meta;
  228. }
  229. else {
  230. if ( isset( $old[ $key ] ) && preg_match( $regex, $old[ $key ] ) ) {
  231. $clean[ $key ] = $old[ $key ];
  232. }
  233. if ( function_exists( 'add_settings_error' ) ) {
  234. add_settings_error(
  235. $this->group_name, // Slug title of the setting.
  236. '_' . $key, // Suffix-id for the error message box.
  237. /* translators: 1: Verification string from user input; 2: Service name. */
  238. sprintf( __( '%1$s does not seem to be a valid %2$s verification string. Please correct.', 'wordpress-seo' ), '<strong>' . esc_html( $meta ) . '</strong>', $service ), // The error message.
  239. 'error' // Error type, either 'error' or 'updated'.
  240. );
  241. }
  242. }
  243. }
  244. }
  245. }
  246. /**
  247. * @param string $key Key to check, by type of service.
  248. * @param array $dirty Dirty data.
  249. * @param array $old Old data.
  250. * @param array $clean Clean data by reference.
  251. */
  252. public function validate_url( $key, $dirty, $old, &$clean ) {
  253. if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
  254. $url = WPSEO_Utils::sanitize_url( $dirty[ $key ] );
  255. if ( $url !== '' ) {
  256. $clean[ $key ] = $url;
  257. }
  258. else {
  259. if ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) {
  260. $url = WPSEO_Utils::sanitize_url( $old[ $key ] );
  261. if ( $url !== '' ) {
  262. $clean[ $key ] = $url;
  263. }
  264. }
  265. if ( function_exists( 'add_settings_error' ) ) {
  266. $url = WPSEO_Utils::sanitize_url( $dirty[ $key ] );
  267. add_settings_error(
  268. $this->group_name, // Slug title of the setting.
  269. '_' . $key, // Suffix-id for the error message box.
  270. sprintf(
  271. /* translators: %s expands to an invalid URL. */
  272. __( '%s does not seem to be a valid url. Please correct.', 'wordpress-seo' ),
  273. '<strong>' . esc_html( $url ) . '</strong>'
  274. ), // The error message.
  275. 'error' // Error type, either 'error' or 'updated'.
  276. );
  277. }
  278. }
  279. }
  280. }
  281. /**
  282. * Remove the default filters.
  283. * Called from the validate() method to prevent failure to add new options.
  284. *
  285. * @return void
  286. */
  287. public function remove_default_filters() {
  288. remove_filter( 'default_option_' . $this->option_name, array( $this, 'get_defaults' ) );
  289. }
  290. /**
  291. * Get the enriched default value for an option.
  292. *
  293. * Checks if the concrete class contains an enrich_defaults() method and if so, runs it.
  294. *
  295. * {@internal The enrich_defaults method is used to set defaults for variable array keys
  296. * in an option, such as array keys depending on post_types and/or taxonomies.}}
  297. *
  298. * @return array
  299. */
  300. public function get_defaults() {
  301. if ( method_exists( $this, 'translate_defaults' ) ) {
  302. $this->translate_defaults();
  303. }
  304. if ( method_exists( $this, 'enrich_defaults' ) ) {
  305. $this->enrich_defaults();
  306. }
  307. return apply_filters( 'wpseo_defaults', $this->defaults, $this->option_name );
  308. }
  309. /**
  310. * Add filters to make sure that the option is merged with its defaults before being returned.
  311. *
  312. * @return void
  313. */
  314. public function add_option_filters() {
  315. // Don't change, needs to check for false as could return prio 0 which would evaluate to false.
  316. if ( has_filter( 'option_' . $this->option_name, array( $this, 'get_option' ) ) === false ) {
  317. add_filter( 'option_' . $this->option_name, array( $this, 'get_option' ) );
  318. }
  319. }
  320. /**
  321. * Remove the option filters.
  322. * Called from the clean_up methods to make sure we retrieve the original old option.
  323. *
  324. * @return void
  325. */
  326. public function remove_option_filters() {
  327. remove_filter( 'option_' . $this->option_name, array( $this, 'get_option' ) );
  328. }
  329. /**
  330. * Merge an option with its default values.
  331. *
  332. * This method should *not* be called directly!!! It is only meant to filter the get_option() results.
  333. *
  334. * @param mixed $options Option value.
  335. *
  336. * @return mixed Option merged with the defaults for that option.
  337. */
  338. public function get_option( $options = null ) {
  339. $filtered = $this->array_filter_merge( $options );
  340. /*
  341. * If the option contains variable option keys, make sure we don't remove those settings
  342. * - even if the defaults are not complete yet.
  343. * Unfortunately this means we also won't be removing the settings for post types or taxonomies
  344. * which are no longer in the WP install, but rather that than the other way around.
  345. */
  346. if ( isset( $this->variable_array_key_patterns ) ) {
  347. $filtered = $this->retain_variable_keys( $options, $filtered );
  348. }
  349. return $filtered;
  350. }
  351. /* *********** METHODS influencing add_uption(), update_option() and saving from admin pages. *********** */
  352. /**
  353. * Register (whitelist) the option for the configuration pages.
  354. * The validation callback is already registered separately on the sanitize_option hook,
  355. * so no need to double register.
  356. *
  357. * @return void
  358. */
  359. public function register_setting() {
  360. if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) {
  361. return;
  362. }
  363. if ( $this->multisite_only === true ) {
  364. $network_settings_api = Yoast_Network_Settings_API::get();
  365. if ( $network_settings_api->meets_requirements() ) {
  366. $network_settings_api->register_setting( $this->group_name, $this->option_name );
  367. }
  368. return;
  369. }
  370. register_setting( $this->group_name, $this->option_name );
  371. }
  372. /**
  373. * Validate the option
  374. *
  375. * @param mixed $option_value The unvalidated new value for the option.
  376. *
  377. * @return array Validated new value for the option.
  378. */
  379. public function validate( $option_value ) {
  380. $clean = $this->get_defaults();
  381. /* Return the defaults if the new value is empty. */
  382. if ( ! is_array( $option_value ) || $option_value === array() ) {
  383. return $clean;
  384. }
  385. $option_value = array_map( array( 'WPSEO_Utils', 'trim_recursive' ), $option_value );
  386. if ( $this->multisite_only !== true ) {
  387. $old = get_option( $this->option_name );
  388. }
  389. else {
  390. $old = get_site_option( $this->option_name );
  391. }
  392. $clean = $this->validate_option( $option_value, $clean, $old );
  393. /* Retain the values for variable array keys even when the post type/taxonomy is not yet registered. */
  394. if ( isset( $this->variable_array_key_patterns ) ) {
  395. $clean = $this->retain_variable_keys( $option_value, $clean );
  396. }
  397. $this->remove_default_filters();
  398. return $clean;
  399. }
  400. /**
  401. * All concrete classes must contain a validate_option() method which validates all
  402. * values within the option.
  403. *
  404. * @param array $dirty New value for the option.
  405. * @param array $clean Clean value for the option, normally the defaults.
  406. * @param array $old Old value of the option.
  407. */
  408. abstract protected function validate_option( $dirty, $clean, $old );
  409. /* *********** METHODS for ADDING/UPDATING/UPGRADING the option. *********** */
  410. /**
  411. * Retrieve the real old value (unmerged with defaults).
  412. *
  413. * @return array|bool The original option value (which can be false if the option doesn't exist).
  414. */
  415. protected function get_original_option() {
  416. $this->remove_default_filters();
  417. $this->remove_option_filters();
  418. // Get (unvalidated) array, NOT merged with defaults.
  419. if ( $this->multisite_only !== true ) {
  420. $option_value = get_option( $this->option_name );
  421. }
  422. else {
  423. $option_value = get_site_option( $this->option_name );
  424. }
  425. $this->add_option_filters();
  426. $this->add_default_filters();
  427. return $option_value;
  428. }
  429. /**
  430. * Add the option if it doesn't exist for some strange reason.
  431. *
  432. * @uses WPSEO_Option::get_original_option()
  433. *
  434. * @return void
  435. */
  436. public function maybe_add_option() {
  437. if ( $this->get_original_option() === false ) {
  438. if ( $this->multisite_only !== true ) {
  439. update_option( $this->option_name, $this->get_defaults() );
  440. }
  441. else {
  442. $this->update_site_option( $this->get_defaults() );
  443. }
  444. }
  445. }
  446. /**
  447. * Update a site_option.
  448. *
  449. * {@internal This special method is only needed for multisite options, but very needed indeed there.
  450. * The order in which certain functions and hooks are run is different between
  451. * get_option() and get_site_option() which means in practice that the removing
  452. * of the default filters would be done too late and the re-adding of the default
  453. * filters might not be done at all.
  454. * Aka: use the WPSEO_Options::update_site_option() method (which calls this method)
  455. * for safely adding/updating multisite options.}}
  456. *
  457. * @param mixed $value The new value for the option.
  458. *
  459. * @return bool Whether the update was succesfull.
  460. */
  461. public function update_site_option( $value ) {
  462. if ( $this->multisite_only === true && is_multisite() ) {
  463. $this->remove_default_filters();
  464. $result = update_site_option( $this->option_name, $value );
  465. $this->add_default_filters();
  466. return $result;
  467. }
  468. else {
  469. return false;
  470. }
  471. }
  472. /**
  473. * Retrieve the real old value (unmerged with defaults), clean and re-save the option.
  474. *
  475. * @uses WPSEO_Option::get_original_option()
  476. * @uses WPSEO_Option::import()
  477. *
  478. * @param string $current_version Optional. Version from which to upgrade, if not set, version specific upgrades will be disregarded.
  479. *
  480. * @return void
  481. */
  482. public function clean( $current_version = null ) {
  483. $option_value = $this->get_original_option();
  484. $this->import( $option_value, $current_version );
  485. }
  486. /**
  487. * Clean and re-save the option.
  488. *
  489. * @uses clean_option() method from concrete class if it exists.
  490. *
  491. * @todo [JRF/whomever] Figure out a way to show settings error during/after the upgrade - maybe
  492. * something along the lines of:
  493. * -> add them to a property in this class
  494. * -> if that property isset at the end of the routine and add_settings_error function does not exist,
  495. * save as transient (or update the transient if one already exists)
  496. * -> next time an admin is in the WP back-end, show the errors and delete the transient or only delete it
  497. * once the admin has dismissed the message (add ajax function)
  498. * Important: all validation routines which add_settings_errors would need to be changed for this to work
  499. *
  500. * @param array $option_value Option value to be imported.
  501. * @param string $current_version Optional. Version from which to upgrade, if not set, version specific upgrades will be disregarded.
  502. * @param array $all_old_option_values Optional. Only used when importing old options to have access to the real old values, in contrast to the saved ones.
  503. *
  504. * @return void
  505. */
  506. public function import( $option_value, $current_version = null, $all_old_option_values = null ) {
  507. if ( $option_value === false ) {
  508. $option_value = $this->get_defaults();
  509. }
  510. elseif ( is_array( $option_value ) && method_exists( $this, 'clean_option' ) ) {
  511. $option_value = $this->clean_option( $option_value, $current_version, $all_old_option_values );
  512. }
  513. /*
  514. * Save the cleaned value - validation will take care of cleaning out array keys which
  515. * should no longer be there.
  516. */
  517. if ( $this->multisite_only !== true ) {
  518. update_option( $this->option_name, $option_value );
  519. }
  520. else {
  521. $this->update_site_option( $this->option_name, $option_value );
  522. }
  523. }
  524. /**
  525. * Returns the variable array key patterns for an options class.
  526. *
  527. * @return array
  528. */
  529. public function get_patterns() {
  530. return (array) $this->variable_array_key_patterns;
  531. }
  532. /**
  533. * Concrete classes *may* contain a clean_option method which will clean out old/renamed
  534. * values within the option.
  535. */
  536. // abstract public function clean_option( $option_value, $current_version = null, $all_old_option_values = null );
  537. /* *********** HELPER METHODS for internal use. *********** */
  538. /**
  539. * Helper method - Combines a fixed array of default values with an options array
  540. * while filtering out any keys which are not in the defaults array.
  541. *
  542. * @todo [JRF] - shouldn't this be a straight array merge ? at the end of the day, the validation
  543. * removes any invalid keys on save.
  544. *
  545. * @param array $options Optional. Current options. If not set, the option defaults for the $option_key will be returned.
  546. *
  547. * @return array Combined and filtered options array.
  548. */
  549. protected function array_filter_merge( $options = null ) {
  550. $defaults = $this->get_defaults();
  551. if ( ! isset( $options ) || $options === false || $options === array() ) {
  552. return $defaults;
  553. }
  554. $options = (array) $options;
  555. /*
  556. $filtered = array();
  557. if ( $defaults !== array() ) {
  558. foreach ( $defaults as $key => $default_value ) {
  559. // @todo should this walk through array subkeys ?
  560. $filtered[ $key ] = ( isset( $options[ $key ] ) ? $options[ $key ] : $default_value );
  561. }
  562. }
  563. */
  564. $filtered = array_merge( $defaults, $options );
  565. return $filtered;
  566. }
  567. /**
  568. * Make sure that any set option values relating to post_types and/or taxonomies are retained,
  569. * even when that post_type or taxonomy may not yet have been registered.
  570. *
  571. * {@internal The wpseo_titles concrete class overrules this method. Make sure that any
  572. * changes applied here, also get ported to that version.}}
  573. *
  574. * @param array $dirty Original option as retrieved from the database.
  575. * @param array $clean Filtered option where any options which shouldn't be in our option
  576. * have already been removed and any options which weren't set
  577. * have been set to their defaults.
  578. *
  579. * @return array
  580. */
  581. protected function retain_variable_keys( $dirty, $clean ) {
  582. if ( ( is_array( $this->variable_array_key_patterns ) && $this->variable_array_key_patterns !== array() ) && ( is_array( $dirty ) && $dirty !== array() ) ) {
  583. foreach ( $dirty as $key => $value ) {
  584. // Do nothing if already in filtered options.
  585. if ( isset( $clean[ $key ] ) ) {
  586. continue;
  587. }
  588. foreach ( $this->variable_array_key_patterns as $pattern ) {
  589. if ( strpos( $key, $pattern ) === 0 ) {
  590. $clean[ $key ] = $value;
  591. break;
  592. }
  593. }
  594. }
  595. }
  596. return $clean;
  597. }
  598. /**
  599. * Check whether a given array key conforms to one of the variable array key patterns for this option.
  600. *
  601. * @usedby validate_option() methods for options with variable array keys.
  602. *
  603. * @param string $key Array key to check.
  604. *
  605. * @return string Pattern if it conforms, original array key if it doesn't or if the option
  606. * does not have variable array keys.
  607. */
  608. protected function get_switch_key( $key ) {
  609. if ( ! isset( $this->variable_array_key_patterns ) || ( ! is_array( $this->variable_array_key_patterns ) || $this->variable_array_key_patterns === array() ) ) {
  610. return $key;
  611. }
  612. foreach ( $this->variable_array_key_patterns as $pattern ) {
  613. if ( strpos( $key, $pattern ) === 0 ) {
  614. return $pattern;
  615. }
  616. }
  617. return $key;
  618. }
  619. }