class-wpseo-option.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  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. * [Important information about add/updating/changing these classes]
  32. * - Make sure that option array key names are unique across options. The WPSEO_Options::get_all()
  33. * method merges most options together. If any of them have non-unique names, even if they
  34. * are in a different option, they *will* overwrite each other.
  35. * - When you add a new array key in an option: make sure you add proper defaults and add the key
  36. * to the validation routine in the proper place or add a new validation case.
  37. * You don't need to do any upgrading as any option returned will always be merged with the
  38. * defaults, so new options will automatically be available.
  39. * If the default value is a string which need translating, add this to the concrete class
  40. * translate_defaults() method.
  41. * - When you remove an array key from an option: if it's important that the option is really removed,
  42. * add the WPSEO_Option::clean_up( $option_name ) method to the upgrade run.
  43. * This will re-save the option and automatically remove the array key no longer in existance.
  44. * - When you rename a sub-option: add it to the clean_option() routine and run that in the upgrade run.
  45. * - When you change the default for an option sub-key, make sure you verify that the validation routine will
  46. * still work the way it should.
  47. * Example: changing a default from '' (empty string) to 'text' with a validation routine with tests
  48. * for an empty string will prevent a user from saving an empty string as the real value. So the
  49. * test for '' with the validation routine would have to be removed in that case.
  50. * - If an option needs specific actions different from defined in this abstract class, you can just overrule
  51. * a method by defining it in the concrete class.
  52. *
  53. * @todo [JRF => testers] Double check that validation will not cause errors when called
  54. * from upgrade routine (some of the WP functions may not yet be available).
  55. */
  56. abstract class WPSEO_Option {
  57. /**
  58. * Prefix for override option keys that allow or disallow the option key of the same name.
  59. *
  60. * @var string
  61. */
  62. const ALLOW_KEY_PREFIX = 'allow_';
  63. /**
  64. * Option name - MUST be set in concrete class and set to public.
  65. *
  66. * @var string
  67. */
  68. protected $option_name;
  69. /**
  70. * Option group name for use in settings forms.
  71. *
  72. * Will be set automagically if not set in concrete class (i.e.
  73. * if it confirm to the normal pattern 'yoast' . $option_name . 'options',
  74. * only set in conrete class if it doesn't).
  75. *
  76. * @var string
  77. */
  78. public $group_name;
  79. /**
  80. * Whether to include the option in the return for WPSEO_Options::get_all().
  81. *
  82. * Also determines which options are copied over for ms_(re)set_blog().
  83. *
  84. * @var bool
  85. */
  86. public $include_in_all = true;
  87. /**
  88. * Whether this option is only for when the install is multisite.
  89. *
  90. * @var bool
  91. */
  92. public $multisite_only = false;
  93. /**
  94. * Array of defaults for the option - MUST be set in concrete class.
  95. *
  96. * Shouldn't be requested directly, use $this->get_defaults();
  97. *
  98. * @var array
  99. */
  100. protected $defaults;
  101. /**
  102. * Array of variable option name patterns for the option - if any -.
  103. *
  104. * Set this when the option contains array keys which vary based on post_type
  105. * or taxonomy.
  106. *
  107. * @var array
  108. */
  109. protected $variable_array_key_patterns;
  110. /**
  111. * Array of sub-options which should not be overloaded with multi-site defaults.
  112. *
  113. * @var array
  114. */
  115. public $ms_exclude = [];
  116. /**
  117. * Name for an option higher in the hierarchy to override setting access.
  118. *
  119. * @var string
  120. */
  121. protected $override_option_name;
  122. /**
  123. * Instance of this class.
  124. *
  125. * @var object
  126. */
  127. protected static $instance;
  128. /* *********** INSTANTIATION METHODS *********** */
  129. /**
  130. * Add all the actions and filters for the option.
  131. *
  132. * @return \WPSEO_Option
  133. */
  134. protected function __construct() {
  135. /* Add filters which get applied to the get_options() results. */
  136. $this->add_default_filters(); // Return defaults if option not set.
  137. $this->add_option_filters(); // Merge with defaults if option *is* set.
  138. if ( $this->multisite_only !== true ) {
  139. /**
  140. * The option validation routines remove the default filters to prevent failing
  141. * to insert an option if it's new. Let's add them back afterwards.
  142. */
  143. add_action( 'add_option', [ $this, 'add_default_filters' ] ); // Adding back after INSERT.
  144. add_action( 'update_option', [ $this, 'add_default_filters' ] );
  145. }
  146. elseif ( is_multisite() ) {
  147. /*
  148. * The option validation routines remove the default filters to prevent failing
  149. * to insert an option if it's new. Let's add them back afterwards.
  150. *
  151. * For site_options, this method is not foolproof as these actions are not fired
  152. * on an insert/update failure. Please use the WPSEO_Options::update_site_option() method
  153. * for updating site options to make sure the filters are in place.
  154. */
  155. add_action( 'add_site_option_' . $this->option_name, [ $this, 'add_default_filters' ] );
  156. add_action( 'update_site_option_' . $this->option_name, [ $this, 'add_default_filters' ] );
  157. }
  158. /*
  159. * Make sure the option will always get validated, independently of register_setting()
  160. * (only available on back-end).
  161. */
  162. add_filter( 'sanitize_option_' . $this->option_name, [ $this, 'validate' ] );
  163. // Flushes the rewrite rules when option is updated.
  164. add_action( 'update_option_' . $this->option_name, [ 'WPSEO_Utils', 'clear_rewrites' ] );
  165. /* Register our option for the admin pages */
  166. add_action( 'admin_init', [ $this, 'register_setting' ] );
  167. /* Set option group name if not given */
  168. if ( ! isset( $this->group_name ) || $this->group_name === '' ) {
  169. $this->group_name = 'yoast_' . $this->option_name . '_options';
  170. }
  171. /* Translate some defaults as early as possible - textdomain is loaded in init on priority 1. */
  172. if ( method_exists( $this, 'translate_defaults' ) ) {
  173. add_action( 'init', [ $this, 'translate_defaults' ], 2 );
  174. }
  175. /**
  176. * Enrich defaults once custom post types and taxonomies have been registered
  177. * which is normally done on the init action.
  178. *
  179. * @todo [JRF/testers] Verify that none of the options which are only available after
  180. * enrichment are used before the enriching.
  181. */
  182. if ( method_exists( $this, 'enrich_defaults' ) ) {
  183. add_action( 'init', [ $this, 'enrich_defaults' ], 99 );
  184. }
  185. }
  186. // @codingStandardsIgnoreStart
  187. /**
  188. * All concrete classes *must* contain the get_instance method.
  189. *
  190. * {@internal Unfortunately I can't define it as an abstract as it also *has* to be static...}}
  191. */
  192. // abstract protected static function get_instance();
  193. /**
  194. * Concrete classes *may* contain a translate_defaults method.
  195. */
  196. // abstract public function translate_defaults();
  197. /**
  198. * Concrete classes *may* contain a enrich_defaults method to add additional defaults once
  199. * all post_types and taxonomies have been registered.
  200. */
  201. // abstract public function enrich_defaults();
  202. /* *********** METHODS INFLUENCING get_option() *********** */
  203. /**
  204. * Add filters to make sure that the option default is returned if the option is not set.
  205. *
  206. * @return void
  207. */
  208. public function add_default_filters() {
  209. // Don't change, needs to check for false as could return prio 0 which would evaluate to false.
  210. if ( has_filter( 'default_option_' . $this->option_name, array( $this, 'get_defaults' ) ) === false ) {
  211. add_filter( 'default_option_' . $this->option_name, array( $this, 'get_defaults' ) );
  212. }
  213. }
  214. // @codingStandardsIgnoreStart
  215. /**
  216. * Validate webmaster tools & Pinterest verification strings.
  217. *
  218. * @param string $key Key to check, by type of service.
  219. * @param array $dirty Dirty data with the new values.
  220. * @param array $old Old data.
  221. * @param array $clean Clean data by reference, normally the default values.
  222. */
  223. public function validate_verification_string( $key, $dirty, $old, &$clean ) {
  224. if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
  225. $meta = $dirty[ $key ];
  226. if ( strpos( $meta, 'content=' ) ) {
  227. // Make sure we only have the real key, not a complete meta tag.
  228. preg_match( '`content=([\'"])?([^\'"> ]+)(?:\1|[ />])`', $meta, $match );
  229. if ( isset( $match[2] ) ) {
  230. $meta = $match[2];
  231. }
  232. unset( $match );
  233. }
  234. $meta = sanitize_text_field( $meta );
  235. if ( $meta !== '' ) {
  236. $regex = '`^[A-Fa-f0-9_-]+$`';
  237. $service = '';
  238. switch ( $key ) {
  239. case 'baiduverify':
  240. $regex = '`^[A-Za-z0-9_-]+$`';
  241. $service = 'Baidu Webmaster tools';
  242. break;
  243. case 'googleverify':
  244. $regex = '`^[A-Za-z0-9_-]+$`';
  245. $service = 'Google Webmaster tools';
  246. break;
  247. case 'msverify':
  248. $service = 'Bing Webmaster tools';
  249. break;
  250. case 'pinterestverify':
  251. $service = 'Pinterest';
  252. break;
  253. case 'yandexverify':
  254. $service = 'Yandex Webmaster tools';
  255. break;
  256. }
  257. if ( preg_match( $regex, $meta ) ) {
  258. $clean[ $key ] = $meta;
  259. }
  260. else {
  261. // Restore the previous value, if any.
  262. if ( isset( $old[ $key ] ) && preg_match( $regex, $old[ $key ] ) ) {
  263. $clean[ $key ] = $old[ $key ];
  264. }
  265. if ( function_exists( 'add_settings_error' ) ) {
  266. add_settings_error(
  267. $this->group_name, // Slug title of the setting.
  268. $key, // Suffix-ID for the error message box. WordPress prepends `setting-error-`.
  269. /* translators: 1: Verification string from user input; 2: Service name. */
  270. 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.
  271. 'notice-error' // CSS class for the WP notice, either the legacy 'error' / 'updated' or the new `notice-*` ones.
  272. );
  273. }
  274. Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $meta );
  275. }
  276. }
  277. }
  278. }
  279. /**
  280. * Validates an option as a valid URL. Prints out a WordPress settings error
  281. * notice if the URL is invalid.
  282. *
  283. * @param string $key Key to check, by type of URL setting.
  284. * @param array $dirty Dirty data with the new values.
  285. * @param array $old Old data.
  286. * @param array $clean Clean data by reference, normally the default values.
  287. */
  288. public function validate_url( $key, $dirty, $old, &$clean ) {
  289. if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
  290. $submitted_url = trim( htmlspecialchars( $dirty[ $key ], ENT_COMPAT, get_bloginfo( 'charset' ), true ) );
  291. $validated_url = filter_var( WPSEO_Utils::sanitize_url( $submitted_url ), FILTER_VALIDATE_URL );
  292. if ( $validated_url === false ) {
  293. if ( function_exists( 'add_settings_error' ) ) {
  294. add_settings_error(
  295. // Slug title of the setting.
  296. $this->group_name,
  297. // Suffix-ID for the error message box. WordPress prepends `setting-error-`.
  298. $key,
  299. sprintf(
  300. /* translators: %s expands to an invalid URL. */
  301. __( '%s does not seem to be a valid url. Please correct.', 'wordpress-seo' ),
  302. '<strong>' . esc_html( $submitted_url ) . '</strong>'
  303. ),
  304. // CSS class for the WP notice.
  305. 'notice-error'
  306. );
  307. }
  308. // Restore the previous URL value, if any.
  309. if ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) {
  310. $url = WPSEO_Utils::sanitize_url( $old[ $key ] );
  311. if ( $url !== '' ) {
  312. $clean[ $key ] = $url;
  313. }
  314. }
  315. Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $submitted_url );
  316. return;
  317. }
  318. // The URL format is valid, let's sanitize it.
  319. $url = WPSEO_Utils::sanitize_url( $validated_url );
  320. if ( $url !== '' ) {
  321. $clean[ $key ] = $url;
  322. }
  323. }
  324. }
  325. /**
  326. * Validates a Facebook App ID.
  327. *
  328. * @param string $key Key to check, in this case: the Facebook App ID field name.
  329. * @param array $dirty Dirty data with the new values.
  330. * @param array $old Old data.
  331. * @param array $clean Clean data by reference, normally the default values.
  332. */
  333. public function validate_facebook_app_id( $key, $dirty, $old, &$clean ) {
  334. if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
  335. $url = 'https://graph.facebook.com/' . $dirty[ $key ];
  336. $response = wp_remote_get( $url );
  337. // These filters are used in the tests.
  338. /**
  339. * Filter: 'validate_facebook_app_id_api_response_code' - Allows to filter the Faceboook API response code.
  340. *
  341. * @api int $response_code The Facebook API response header code.
  342. */
  343. $response_code = apply_filters( 'validate_facebook_app_id_api_response_code', wp_remote_retrieve_response_code( $response ) );
  344. /**
  345. * Filter: 'validate_facebook_app_id_api_response_body' - Allows to filter the Faceboook API response body.
  346. *
  347. * @api string $response_body The Facebook API JSON response body.
  348. */
  349. $response_body = apply_filters( 'validate_facebook_app_id_api_response_body', wp_remote_retrieve_body( $response ) );
  350. $response_object = json_decode( $response_body );
  351. /*
  352. * When the request is successful the response code will be 200 and
  353. * the response object will contain an `id` property.
  354. */
  355. if ( $response_code === 200 && isset( $response_object->id ) ) {
  356. $clean[ $key ] = $dirty[ $key ];
  357. return;
  358. }
  359. // Restore the previous value, if any.
  360. if ( isset( $old[ $key ] ) && $old[ $key ] !== '' ) {
  361. $clean[ $key ] = $old[ $key ];
  362. }
  363. if ( function_exists( 'add_settings_error' ) ) {
  364. add_settings_error(
  365. $this->group_name, // Slug title of the setting.
  366. $key, // Suffix-ID for the error message box. WordPress prepends `setting-error-`.
  367. sprintf(
  368. /* translators: %s expands to an invalid Facebook App ID. */
  369. __( '%s does not seem to be a valid Facebook App ID. Please correct.', 'wordpress-seo' ),
  370. '<strong>' . esc_html( $dirty[ $key ] ) . '</strong>'
  371. ), // The error message.
  372. 'notice-error' // CSS class for the WP notice, either the legacy 'error' / 'updated' or the new `notice-*` ones.
  373. );
  374. }
  375. Yoast_Input_Validation::add_dirty_value_to_settings_errors( $key, $dirty[ $key ] );
  376. }
  377. }
  378. /**
  379. * Remove the default filters.
  380. * Called from the validate() method to prevent failure to add new options.
  381. *
  382. * @return void
  383. */
  384. public function remove_default_filters() {
  385. remove_filter( 'default_option_' . $this->option_name, array( $this, 'get_defaults' ) );
  386. }
  387. /**
  388. * Get the enriched default value for an option.
  389. *
  390. * Checks if the concrete class contains an enrich_defaults() method and if so, runs it.
  391. *
  392. * {@internal The enrich_defaults method is used to set defaults for variable array keys
  393. * in an option, such as array keys depending on post_types and/or taxonomies.}}
  394. *
  395. * @return array
  396. */
  397. public function get_defaults() {
  398. if ( method_exists( $this, 'translate_defaults' ) ) {
  399. $this->translate_defaults();
  400. }
  401. if ( method_exists( $this, 'enrich_defaults' ) ) {
  402. $this->enrich_defaults();
  403. }
  404. return apply_filters( 'wpseo_defaults', $this->defaults, $this->option_name );
  405. }
  406. /**
  407. * Add filters to make sure that the option is merged with its defaults before being returned.
  408. *
  409. * @return void
  410. */
  411. public function add_option_filters() {
  412. // Don't change, needs to check for false as could return prio 0 which would evaluate to false.
  413. if ( has_filter( 'option_' . $this->option_name, array( $this, 'get_option' ) ) === false ) {
  414. add_filter( 'option_' . $this->option_name, array( $this, 'get_option' ) );
  415. }
  416. }
  417. /**
  418. * Remove the option filters.
  419. * Called from the clean_up methods to make sure we retrieve the original old option.
  420. *
  421. * @return void
  422. */
  423. public function remove_option_filters() {
  424. remove_filter( 'option_' . $this->option_name, array( $this, 'get_option' ) );
  425. }
  426. /**
  427. * Merge an option with its default values.
  428. *
  429. * This method should *not* be called directly!!! It is only meant to filter the get_option() results.
  430. *
  431. * @param mixed $options Option value.
  432. *
  433. * @return mixed Option merged with the defaults for that option.
  434. */
  435. public function get_option( $options = null ) {
  436. $filtered = $this->array_filter_merge( $options );
  437. /*
  438. * If the option contains variable option keys, make sure we don't remove those settings
  439. * - even if the defaults are not complete yet.
  440. * Unfortunately this means we also won't be removing the settings for post types or taxonomies
  441. * which are no longer in the WP install, but rather that than the other way around.
  442. */
  443. if ( isset( $this->variable_array_key_patterns ) ) {
  444. $filtered = $this->retain_variable_keys( $options, $filtered );
  445. }
  446. return $filtered;
  447. }
  448. /* *********** METHODS influencing add_uption(), update_option() and saving from admin pages. *********** */
  449. /**
  450. * Register (whitelist) the option for the configuration pages.
  451. * The validation callback is already registered separately on the sanitize_option hook,
  452. * so no need to double register.
  453. *
  454. * @return void
  455. */
  456. public function register_setting() {
  457. if ( ! WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ) ) {
  458. return;
  459. }
  460. if ( $this->multisite_only === true ) {
  461. $network_settings_api = Yoast_Network_Settings_API::get();
  462. if ( $network_settings_api->meets_requirements() ) {
  463. $network_settings_api->register_setting( $this->group_name, $this->option_name );
  464. }
  465. return;
  466. }
  467. register_setting( $this->group_name, $this->option_name );
  468. }
  469. /**
  470. * Validate the option
  471. *
  472. * @param mixed $option_value The unvalidated new value for the option.
  473. *
  474. * @return array Validated new value for the option.
  475. */
  476. public function validate( $option_value ) {
  477. $clean = $this->get_defaults();
  478. /* Return the defaults if the new value is empty. */
  479. if ( ! is_array( $option_value ) || $option_value === array() ) {
  480. return $clean;
  481. }
  482. $option_value = array_map( array( 'WPSEO_Utils', 'trim_recursive' ), $option_value );
  483. $old = $this->get_original_option();
  484. if ( ! is_array( $old ) ) {
  485. $old = array();
  486. }
  487. $old = array_merge( $clean, $old );
  488. $clean = $this->validate_option( $option_value, $clean, $old );
  489. // Prevent updates to variables that are disabled via the override option.
  490. $clean = $this->prevent_disabled_options_update( $clean, $old );
  491. /* Retain the values for variable array keys even when the post type/taxonomy is not yet registered. */
  492. if ( isset( $this->variable_array_key_patterns ) ) {
  493. $clean = $this->retain_variable_keys( $option_value, $clean );
  494. }
  495. $this->remove_default_filters();
  496. return $clean;
  497. }
  498. /**
  499. * Checks whether a specific option key is disabled.
  500. *
  501. * This is determined by whether an override option is available with a key that equals the given key prefixed
  502. * with 'allow_'.
  503. *
  504. * @param string $key Option key.
  505. *
  506. * @return bool True if option key is disabled, false otherwise.
  507. */
  508. public function is_disabled( $key ) {
  509. $override_option = $this->get_override_option();
  510. if ( empty( $override_option ) ) {
  511. return false;
  512. }
  513. return isset( $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) && ! $override_option[ self::ALLOW_KEY_PREFIX . $key ];
  514. }
  515. /**
  516. * All concrete classes must contain a validate_option() method which validates all
  517. * values within the option.
  518. *
  519. * @param array $dirty New value for the option.
  520. * @param array $clean Clean value for the option, normally the defaults.
  521. * @param array $old Old value of the option.
  522. */
  523. abstract protected function validate_option( $dirty, $clean, $old );
  524. /* *********** METHODS for ADDING/UPDATING/UPGRADING the option. *********** */
  525. /**
  526. * Retrieve the real old value (unmerged with defaults).
  527. *
  528. * @return array|bool The original option value (which can be false if the option doesn't exist).
  529. */
  530. protected function get_original_option() {
  531. $this->remove_default_filters();
  532. $this->remove_option_filters();
  533. // Get (unvalidated) array, NOT merged with defaults.
  534. if ( $this->multisite_only !== true ) {
  535. $option_value = get_option( $this->option_name );
  536. }
  537. else {
  538. $option_value = get_site_option( $this->option_name );
  539. }
  540. $this->add_option_filters();
  541. $this->add_default_filters();
  542. return $option_value;
  543. }
  544. /**
  545. * Add the option if it doesn't exist for some strange reason.
  546. *
  547. * @uses WPSEO_Option::get_original_option()
  548. *
  549. * @return void
  550. */
  551. public function maybe_add_option() {
  552. if ( $this->get_original_option() === false ) {
  553. if ( $this->multisite_only !== true ) {
  554. update_option( $this->option_name, $this->get_defaults() );
  555. }
  556. else {
  557. $this->update_site_option( $this->get_defaults() );
  558. }
  559. }
  560. }
  561. /**
  562. * Update a site_option.
  563. *
  564. * {@internal This special method is only needed for multisite options, but very needed indeed there.
  565. * The order in which certain functions and hooks are run is different between
  566. * get_option() and get_site_option() which means in practice that the removing
  567. * of the default filters would be done too late and the re-adding of the default
  568. * filters might not be done at all.
  569. * Aka: use the WPSEO_Options::update_site_option() method (which calls this method)
  570. * for safely adding/updating multisite options.}}
  571. *
  572. * @param mixed $value The new value for the option.
  573. *
  574. * @return bool Whether the update was succesfull.
  575. */
  576. public function update_site_option( $value ) {
  577. if ( $this->multisite_only === true && is_multisite() ) {
  578. $this->remove_default_filters();
  579. $result = update_site_option( $this->option_name, $value );
  580. $this->add_default_filters();
  581. return $result;
  582. }
  583. else {
  584. return false;
  585. }
  586. }
  587. /**
  588. * Retrieve the real old value (unmerged with defaults), clean and re-save the option.
  589. *
  590. * @uses WPSEO_Option::get_original_option()
  591. * @uses WPSEO_Option::import()
  592. *
  593. * @param string $current_version Optional. Version from which to upgrade, if not set,
  594. * version specific upgrades will be disregarded.
  595. *
  596. * @return void
  597. */
  598. public function clean( $current_version = null ) {
  599. $option_value = $this->get_original_option();
  600. $this->import( $option_value, $current_version );
  601. }
  602. /**
  603. * Clean and re-save the option.
  604. *
  605. * @uses clean_option() method from concrete class if it exists.
  606. *
  607. * @todo [JRF/whomever] Figure out a way to show settings error during/after the upgrade - maybe
  608. * something along the lines of:
  609. * -> add them to a property in this class
  610. * -> if that property isset at the end of the routine and add_settings_error function does not exist,
  611. * save as transient (or update the transient if one already exists)
  612. * -> next time an admin is in the WP back-end, show the errors and delete the transient or only delete it
  613. * once the admin has dismissed the message (add ajax function)
  614. * Important: all validation routines which add_settings_errors would need to be changed for this to work
  615. *
  616. * @param array $option_value Option value to be imported.
  617. * @param string $current_version Optional. Version from which to upgrade, if not set,
  618. * version specific upgrades will be disregarded.
  619. * @param array $all_old_option_values Optional. Only used when importing old options to
  620. * have access to the real old values, in contrast to
  621. * the saved ones.
  622. *
  623. * @return void
  624. */
  625. public function import( $option_value, $current_version = null, $all_old_option_values = null ) {
  626. if ( $option_value === false ) {
  627. $option_value = $this->get_defaults();
  628. }
  629. elseif ( is_array( $option_value ) && method_exists( $this, 'clean_option' ) ) {
  630. $option_value = $this->clean_option( $option_value, $current_version, $all_old_option_values );
  631. }
  632. /*
  633. * Save the cleaned value - validation will take care of cleaning out array keys which
  634. * should no longer be there.
  635. */
  636. if ( $this->multisite_only !== true ) {
  637. update_option( $this->option_name, $option_value );
  638. }
  639. else {
  640. $this->update_site_option( $this->option_name, $option_value );
  641. }
  642. }
  643. /**
  644. * Returns the variable array key patterns for an options class.
  645. *
  646. * @return array
  647. */
  648. public function get_patterns() {
  649. return (array) $this->variable_array_key_patterns;
  650. }
  651. /**
  652. * Retrieves the option name.
  653. *
  654. * @return string The set option name.
  655. */
  656. public function get_option_name() {
  657. return $this->option_name;
  658. }
  659. /**
  660. * Concrete classes *may* contain a clean_option method which will clean out old/renamed
  661. * values within the option.
  662. */
  663. // abstract public function clean_option( $option_value, $current_version = null, $all_old_option_values = null );
  664. /* *********** HELPER METHODS for internal use. *********** */
  665. /**
  666. * Helper method - Combines a fixed array of default values with an options array
  667. * while filtering out any keys which are not in the defaults array.
  668. *
  669. * @todo [JRF] - shouldn't this be a straight array merge ? at the end of the day, the validation
  670. * removes any invalid keys on save.
  671. *
  672. * @param array $options Optional. Current options. If not set, the option defaults
  673. * for the $option_key will be returned.
  674. *
  675. * @return array Combined and filtered options array.
  676. */
  677. protected function array_filter_merge( $options = null ) {
  678. $defaults = $this->get_defaults();
  679. if ( ! isset( $options ) || $options === false || $options === array() ) {
  680. return $defaults;
  681. }
  682. $options = (array) $options;
  683. /*
  684. $filtered = array();
  685. if ( $defaults !== array() ) {
  686. foreach ( $defaults as $key => $default_value ) {
  687. // @todo should this walk through array subkeys ?
  688. $filtered[ $key ] = ( isset( $options[ $key ] ) ? $options[ $key ] : $default_value );
  689. }
  690. }
  691. */
  692. $filtered = array_merge( $defaults, $options );
  693. return $filtered;
  694. }
  695. /**
  696. * Sets updated values for variables that are disabled via the override option back to their previous values.
  697. *
  698. * @param array $updated Updated option value.
  699. * @param array $old Old option value.
  700. *
  701. * @return array Updated option value, with all disabled variables set to their old values.
  702. */
  703. protected function prevent_disabled_options_update( $updated, $old ) {
  704. $override_option = $this->get_override_option();
  705. if ( empty( $override_option ) ) {
  706. return $updated;
  707. }
  708. /*
  709. * This loop could as well call `is_disabled( $key )` for each iteration,
  710. * however this would be worse performance-wise.
  711. */
  712. foreach ( $old as $key => $value ) {
  713. if ( isset( $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) && ! $override_option[ self::ALLOW_KEY_PREFIX . $key ] ) {
  714. $updated[ $key ] = $old[ $key ];
  715. }
  716. }
  717. return $updated;
  718. }
  719. /**
  720. * Retrieves the value of the override option, if available.
  721. *
  722. * An override option contains values that may determine access to certain sub-variables
  723. * of this option.
  724. *
  725. * Only regular options in multisite can have override options, which in that case
  726. * would be network options.
  727. *
  728. * @return array Override option value, or empty array if unavailable.
  729. */
  730. protected function get_override_option() {
  731. if ( empty( $this->override_option_name ) || $this->multisite_only === true || ! is_multisite() ) {
  732. return array();
  733. }
  734. return get_site_option( $this->override_option_name, array() );
  735. }
  736. /**
  737. * Make sure that any set option values relating to post_types and/or taxonomies are retained,
  738. * even when that post_type or taxonomy may not yet have been registered.
  739. *
  740. * {@internal The wpseo_titles concrete class overrules this method. Make sure that any
  741. * changes applied here, also get ported to that version.}}
  742. *
  743. * @param array $dirty Original option as retrieved from the database.
  744. * @param array $clean Filtered option where any options which shouldn't be in our option
  745. * have already been removed and any options which weren't set
  746. * have been set to their defaults.
  747. *
  748. * @return array
  749. */
  750. protected function retain_variable_keys( $dirty, $clean ) {
  751. if ( ( is_array( $this->variable_array_key_patterns ) && $this->variable_array_key_patterns !== array() ) && ( is_array( $dirty ) && $dirty !== array() ) ) {
  752. foreach ( $dirty as $key => $value ) {
  753. // Do nothing if already in filtered options.
  754. if ( isset( $clean[ $key ] ) ) {
  755. continue;
  756. }
  757. foreach ( $this->variable_array_key_patterns as $pattern ) {
  758. if ( strpos( $key, $pattern ) === 0 ) {
  759. $clean[ $key ] = $value;
  760. break;
  761. }
  762. }
  763. }
  764. }
  765. return $clean;
  766. }
  767. /**
  768. * Check whether a given array key conforms to one of the variable array key patterns for this option.
  769. *
  770. * @usedby validate_option() methods for options with variable array keys.
  771. *
  772. * @param string $key Array key to check.
  773. *
  774. * @return string Pattern if it conforms, original array key if it doesn't or if the option
  775. * does not have variable array keys.
  776. */
  777. protected function get_switch_key( $key ) {
  778. if ( ! isset( $this->variable_array_key_patterns ) || ( ! is_array( $this->variable_array_key_patterns ) || $this->variable_array_key_patterns === array() ) ) {
  779. return $key;
  780. }
  781. foreach ( $this->variable_array_key_patterns as $pattern ) {
  782. if ( strpos( $key, $pattern ) === 0 ) {
  783. return $pattern;
  784. }
  785. }
  786. return $key;
  787. }
  788. }