setting.php 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  1. <?php
  2. /**
  3. * Tests for the WP_Customize_Setting class.
  4. *
  5. * @group customize
  6. */
  7. class Tests_WP_Customize_Setting extends WP_UnitTestCase {
  8. /**
  9. * @var WP_Customize_Manager
  10. */
  11. protected $manager;
  12. /**
  13. * @var stdClass an instance which serves as a symbol to do identity checks with
  14. */
  15. public $undefined;
  16. function setUp() {
  17. parent::setUp();
  18. require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
  19. $GLOBALS['wp_customize'] = new WP_Customize_Manager();
  20. $this->manager = $GLOBALS['wp_customize'];
  21. $this->undefined = new stdClass();
  22. }
  23. function tearDown() {
  24. $this->manager = null;
  25. unset( $GLOBALS['wp_customize'] );
  26. parent::tearDown();
  27. }
  28. function test_constructor_without_args() {
  29. $setting = new WP_Customize_Setting( $this->manager, 'foo' );
  30. $this->assertSame( $this->manager, $setting->manager );
  31. $this->assertSame( 'foo', $setting->id );
  32. $this->assertSame( 'theme_mod', $setting->type );
  33. $this->assertSame( 'edit_theme_options', $setting->capability );
  34. $this->assertSame( '', $setting->theme_supports );
  35. $this->assertSame( '', $setting->default );
  36. $this->assertSame( 'refresh', $setting->transport );
  37. $this->assertSame( '', $setting->sanitize_callback );
  38. $this->assertSame( '', $setting->sanitize_js_callback );
  39. $this->assertFalse( has_filter( "customize_validate_{$setting->id}" ) );
  40. $this->assertFalse( has_filter( "customize_sanitize_{$setting->id}" ) );
  41. $this->assertFalse( has_filter( "customize_sanitize_js_{$setting->id}" ) );
  42. $this->assertFalse( $setting->dirty );
  43. }
  44. /**
  45. * A test validate callback function.
  46. *
  47. * @param mixed $value The setting value.
  48. * @param WP_Customize_Setting $setting The setting object.
  49. */
  50. public function validate_callback_for_tests( $value, $setting ) {
  51. return $value . ':validate_callback';
  52. }
  53. /**
  54. * A test sanitize callback function.
  55. *
  56. * @param mixed $value The setting value.
  57. * @param WP_Customize_Setting $setting The setting object.
  58. */
  59. public function sanitize_callback_for_tests( $value, $setting ) {
  60. return $value . ':sanitize_callback';
  61. }
  62. /**
  63. * A test sanitize JS callback function.
  64. *
  65. * @param mixed $value The setting value.
  66. * @param WP_Customize_Setting $setting The setting object.
  67. */
  68. public function sanitize_js_callback_for_tests( $value, $setting ) {
  69. return $value . ':sanitize_js_callback';
  70. }
  71. /**
  72. * Sanitize JS callback for base64 encoding.
  73. *
  74. * @param mixed $value The setting value.
  75. * @param WP_Customize_Setting $setting The setting object.
  76. */
  77. function sanitize_js_callback_base64_for_testing( $value, $setting ) {
  78. return base64_encode( $value );
  79. }
  80. function test_constructor_with_args() {
  81. $args = array(
  82. 'type' => 'option',
  83. 'capability' => 'edit_posts',
  84. 'theme_supports' => 'widgets',
  85. 'default' => 'barbar',
  86. 'transport' => 'postMessage',
  87. 'validate_callback' => array( $this, 'validate_callback_for_tests' ),
  88. 'sanitize_callback' => array( $this, 'sanitize_callback_for_tests' ),
  89. 'sanitize_js_callback' => array( $this, 'sanitize_js_callback_for_tests' ),
  90. );
  91. $setting = new WP_Customize_Setting( $this->manager, 'bar', $args );
  92. $this->assertSame( 'bar', $setting->id );
  93. foreach ( $args as $key => $value ) {
  94. $this->assertSame( $value, $setting->$key );
  95. }
  96. $this->assertSame( 10, has_filter( "customize_validate_{$setting->id}", $args['validate_callback'] ) );
  97. $this->assertSame( 10, has_filter( "customize_sanitize_{$setting->id}", $args['sanitize_callback'] ) );
  98. $this->assertSame( 10, has_filter( "customize_sanitize_js_{$setting->id}", $args['sanitize_js_callback'] ) );
  99. }
  100. public $post_data_overrides = array(
  101. 'unset_option_overridden' => 'unset_option_post_override_value\\o/',
  102. 'unset_theme_mod_overridden' => 'unset_theme_mod_post_override_value\\o/',
  103. 'set_option_overridden' => 'set_option_post_override_value\\o/',
  104. 'set_theme_mod_overridden' => 'set_theme_mod_post_override_value\\o/',
  105. 'unset_option_multi_overridden[foo]' => 'unset_option_multi_overridden[foo]_post_override_value\\o/',
  106. 'unset_theme_mod_multi_overridden[foo]' => 'unset_theme_mod_multi_overridden[foo]_post_override_value\\o/',
  107. 'set_option_multi_overridden[foo]' => 'set_option_multi_overridden[foo]_post_override_value\\o/',
  108. 'set_theme_mod_multi_overridden[foo]' => 'set_theme_mod_multi_overridden[foo]_post_override_value\\o/',
  109. );
  110. public $standard_type_configs = array(
  111. 'option' => array(
  112. 'getter' => 'get_option',
  113. 'setter' => 'update_option',
  114. ),
  115. 'theme_mod' => array(
  116. 'getter' => 'get_theme_mod',
  117. 'setter' => 'set_theme_mod',
  118. ),
  119. );
  120. /**
  121. * Run assertions on non-multidimensional standard settings.
  122. *
  123. * @see WP_Customize_Setting::value()
  124. */
  125. function test_preview_standard_types_non_multidimensional() {
  126. wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) );
  127. $_POST['customized'] = wp_slash( wp_json_encode( $this->post_data_overrides ) );
  128. // Try non-multidimensional settings.
  129. foreach ( $this->standard_type_configs as $type => $type_options ) {
  130. // Non-multidimensional: See what effect the preview filter has on a non-existent setting (default value should be seen).
  131. $name = "unset_{$type}_without_post_value";
  132. $default = "default_value_{$name}";
  133. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
  134. $this->assertSame( $this->undefined, call_user_func( $type_options['getter'], $name, $this->undefined ) );
  135. $this->assertSame( $default, $setting->value() );
  136. $this->assertTrue( $setting->preview(), 'Preview should not no-op since setting has no existing value.' );
  137. $this->assertSame( $default, call_user_func( $type_options['getter'], $name, $this->undefined ), sprintf( 'Expected %s(%s) to return setting default: %s.', $type_options['getter'], $name, $default ) );
  138. $this->assertSame( $default, $setting->value() );
  139. // Non-multidimensional: See what effect the preview has on an extant setting (default value should not be seen).
  140. $name = "set_{$type}_without_post_value";
  141. $default = "default_value_{$name}";
  142. $initial_value = "initial_value_{$name}";
  143. call_user_func( $type_options['setter'], $name, $initial_value );
  144. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
  145. $this->assertSame( $initial_value, call_user_func( $type_options['getter'], $name ) );
  146. $this->assertSame( $initial_value, $setting->value() );
  147. $this->assertFalse( $setting->preview(), 'Preview should no-op since setting value was extant and no post value was present.' );
  148. $this->assertSame( 0, did_action( "customize_preview_{$setting->id}" ) ); // Only applicable for custom types (not options or theme_mods).
  149. $this->assertSame( 0, did_action( "customize_preview_{$setting->type}" ) ); // Only applicable for custom types (not options or theme_mods).
  150. $this->assertSame( $initial_value, call_user_func( $type_options['getter'], $name ) );
  151. $this->assertSame( $initial_value, $setting->value() );
  152. // Non-multidimensional: Try updating a value that had a no-op preview.
  153. $overridden_value = "overridden_value_$name";
  154. call_user_func( $type_options['setter'], $name, $overridden_value );
  155. $message = 'Initial value should be overridden because initial preview() was no-op due to setting having existing value and/or post value was absent.';
  156. $this->assertSame( $overridden_value, call_user_func( $type_options['getter'], $name ), $message );
  157. $this->assertSame( $overridden_value, $setting->value(), $message );
  158. $this->assertNotEquals( $initial_value, $setting->value(), $message );
  159. // Non-multidimensional: Ensure that setting a post value *after* preview() is called results in the post value being seen (deferred preview).
  160. $post_value = "post_value_for_{$setting->id}_set_after_preview_called";
  161. $this->assertSame( 0, did_action( "customize_post_value_set_{$setting->id}" ) );
  162. $this->manager->set_post_value( $setting->id, $post_value );
  163. $this->assertSame( 1, did_action( "customize_post_value_set_{$setting->id}" ) );
  164. $this->assertNotEquals( $overridden_value, $setting->value() );
  165. $this->assertSame( $post_value, call_user_func( $type_options['getter'], $name ) );
  166. $this->assertSame( $post_value, $setting->value() );
  167. // Non-multidimensional: Test unset setting being overridden by a post value.
  168. $name = "unset_{$type}_overridden";
  169. $default = "default_value_{$name}";
  170. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
  171. $this->assertSame( $this->undefined, call_user_func( $type_options['getter'], $name, $this->undefined ) );
  172. $this->assertSame( $default, $setting->value() );
  173. $this->assertTrue( $setting->preview(), 'Preview applies because setting has post_data_overrides.' ); // Activate post_data.
  174. $this->assertSame( $this->post_data_overrides[ $name ], call_user_func( $type_options['getter'], $name, $this->undefined ) );
  175. $this->assertSame( $this->post_data_overrides[ $name ], $setting->value() );
  176. // Non-multidimensional: Test set setting being overridden by a post value.
  177. $name = "set_{$type}_overridden";
  178. $default = "default_value_{$name}";
  179. $initial_value = "initial_value_{$name}";
  180. call_user_func( $type_options['setter'], $name, $initial_value );
  181. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
  182. $this->assertSame( $initial_value, call_user_func( $type_options['getter'], $name, $this->undefined ) );
  183. $this->assertSame( $initial_value, $setting->value() );
  184. $this->assertTrue( $setting->preview(), 'Preview applies because setting has post_data_overrides.' ); // Activate post_data.
  185. $this->assertSame( 0, did_action( "customize_preview_{$setting->id}" ) ); // Only applicable for custom types (not options or theme_mods).
  186. $this->assertSame( 0, did_action( "customize_preview_{$setting->type}" ) ); // Only applicable for custom types (not options or theme_mods).
  187. $this->assertSame( $this->post_data_overrides[ $name ], call_user_func( $type_options['getter'], $name, $this->undefined ) );
  188. $this->assertSame( $this->post_data_overrides[ $name ], $setting->value() );
  189. }
  190. }
  191. /**
  192. * Run assertions on multidimensional standard settings.
  193. *
  194. * @see WP_Customize_Setting::preview()
  195. * @see WP_Customize_Setting::value()
  196. */
  197. function test_preview_standard_types_multidimensional() {
  198. wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) );
  199. $_POST['customized'] = wp_slash( wp_json_encode( $this->post_data_overrides ) );
  200. foreach ( $this->standard_type_configs as $type => $type_options ) {
  201. // Multidimensional: See what effect the preview filter has on a non-existent setting (default value should be seen).
  202. $base_name = "unset_{$type}_multi";
  203. $name = $base_name . '[foo]';
  204. $default = "default_value_{$name}";
  205. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
  206. $this->assertSame( $this->undefined, call_user_func( $type_options['getter'], $base_name, $this->undefined ) );
  207. $this->assertSame( $default, $setting->value() );
  208. $this->assertTrue( $setting->preview(), "Preview for $setting->id should apply because setting is not in DB." );
  209. $base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined );
  210. $this->assertArrayHasKey( 'foo', $base_value );
  211. $this->assertSame( $default, $base_value['foo'] );
  212. // Multidimensional: See what effect the preview has on an extant setting (default value should not be seen) without post value.
  213. $base_name = "set_{$type}_multi";
  214. $name = $base_name . '[foo]';
  215. $default = "default_value_{$name}";
  216. $initial_value = "initial_value_{$name}";
  217. $base_initial_value = array(
  218. 'foo' => $initial_value,
  219. 'bar' => 'persisted',
  220. );
  221. call_user_func( $type_options['setter'], $base_name, $base_initial_value );
  222. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
  223. $base_value = call_user_func( $type_options['getter'], $base_name, array() );
  224. $this->assertSame( $initial_value, $base_value['foo'] );
  225. $this->assertSame( $initial_value, $setting->value() );
  226. $this->assertFalse( $setting->preview(), "Preview for $setting->id should no-op because setting is in DB and post value is absent." );
  227. $this->assertSame( 0, did_action( "customize_preview_{$setting->id}" ) ); // Only applicable for custom types (not options or theme_mods).
  228. $this->assertSame( 0, did_action( "customize_preview_{$setting->type}" ) ); // Only applicable for custom types (not options or theme_mods).
  229. $base_value = call_user_func( $type_options['getter'], $base_name, array() );
  230. $this->assertSame( $initial_value, $base_value['foo'] );
  231. $this->assertSame( $initial_value, $setting->value() );
  232. // Multidimensional: Ensure that setting a post value *after* preview() is called results in the post value being seen (deferred preview).
  233. $override_value = "post_value_for_{$setting->id}_set_after_preview_called";
  234. $this->manager->set_post_value( $setting->id, $override_value );
  235. $base_value = call_user_func( $type_options['getter'], $base_name, array() );
  236. $this->assertSame( $override_value, $base_value['foo'] );
  237. $this->assertSame( $override_value, $setting->value() );
  238. // Multidimensional: Test unset setting being overridden by a post value.
  239. $base_name = "unset_{$type}_multi_overridden";
  240. $name = $base_name . '[foo]';
  241. $default = "default_value_{$name}";
  242. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
  243. $this->assertSame( $this->undefined, call_user_func( $type_options['getter'], $base_name, $this->undefined ) );
  244. $this->assertSame( $default, $setting->value() );
  245. $this->assertTrue( $setting->preview(), "Preview for $setting->id should apply because a post value is present." );
  246. $this->assertSame( 0, did_action( "customize_preview_{$setting->id}" ) ); // Only applicable for custom types (not options or theme_mods).
  247. $this->assertSame( 0, did_action( "customize_preview_{$setting->type}" ) ); // Only applicable for custom types (not options or theme_mods).
  248. $base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined );
  249. $this->assertArrayHasKey( 'foo', $base_value );
  250. $this->assertSame( $this->post_data_overrides[ $name ], $base_value['foo'] );
  251. // Multidimensional: Test set setting being overridden by a post value.
  252. $base_name = "set_{$type}_multi_overridden";
  253. $name = $base_name . '[foo]';
  254. $default = "default_value_{$name}";
  255. $initial_value = "initial_value_{$name}";
  256. $base_initial_value = array(
  257. 'foo' => $initial_value,
  258. 'bar' => 'persisted',
  259. );
  260. call_user_func( $type_options['setter'], $base_name, $base_initial_value );
  261. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
  262. $base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined );
  263. $this->assertArrayHasKey( 'foo', $base_value );
  264. $this->assertArrayHasKey( 'bar', $base_value );
  265. $this->assertSame( $base_initial_value['foo'], $base_value['foo'] );
  266. $getter = call_user_func( $type_options['getter'], $base_name, $this->undefined );
  267. $this->assertSame( $base_initial_value['bar'], $getter['bar'] );
  268. $this->assertSame( $initial_value, $setting->value() );
  269. $this->assertTrue( $setting->preview(), "Preview for $setting->id should apply because post value is present." );
  270. $this->assertSame( 0, did_action( "customize_preview_{$setting->id}" ) ); // Only applicable for custom types (not options or theme_mods).
  271. $this->assertSame( 0, did_action( "customize_preview_{$setting->type}" ) ); // Only applicable for custom types (not options or theme_mods).
  272. $base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined );
  273. $this->assertArrayHasKey( 'foo', $base_value );
  274. $this->assertSame( $this->post_data_overrides[ $name ], $base_value['foo'] );
  275. $this->assertArrayHasKey( 'bar', call_user_func( $type_options['getter'], $base_name, $this->undefined ) );
  276. $getter = call_user_func( $type_options['getter'], $base_name, $this->undefined );
  277. $this->assertSame( $base_initial_value['bar'], $getter['bar'] );
  278. }
  279. }
  280. /**
  281. * @var array storage for saved custom type data that are tested in self::test_preview_custom_type()
  282. */
  283. protected $custom_type_data_saved;
  284. /**
  285. * @var array storage for previewed custom type data that are tested in self::test_preview_custom_type()
  286. */
  287. protected $custom_type_data_previewed;
  288. function custom_type_getter( $name, $default = null ) {
  289. if ( did_action( "customize_preview_{$name}" ) && array_key_exists( $name, $this->custom_type_data_previewed ) ) {
  290. $value = $this->custom_type_data_previewed[ $name ];
  291. } elseif ( array_key_exists( $name, $this->custom_type_data_saved ) ) {
  292. $value = $this->custom_type_data_saved[ $name ];
  293. } else {
  294. $value = $default;
  295. }
  296. return $value;
  297. }
  298. function custom_type_setter( $name, $value ) {
  299. $this->custom_type_data_saved[ $name ] = $value;
  300. }
  301. /**
  302. * Filter for `customize_value_{$id_base}`.
  303. *
  304. * @param mixed $default
  305. * @param WP_Customize_Setting $setting
  306. *
  307. * @return mixed|null
  308. */
  309. function custom_type_value_filter( $default, $setting = null ) {
  310. $name = preg_replace( '/^customize_value_/', '', current_filter() );
  311. $this->assertInstanceOf( 'WP_Customize_Setting', $setting );
  312. $id_data = $setting->id_data();
  313. $this->assertSame( $name, $id_data['base'] );
  314. return $this->custom_type_getter( $name, $default );
  315. }
  316. /**
  317. * @param WP_Customize_Setting $setting
  318. */
  319. function custom_type_preview( $setting ) {
  320. $previewed_value = $setting->post_value( $this->undefined );
  321. if ( $this->undefined !== $previewed_value ) {
  322. $this->custom_type_data_previewed[ $setting->id ] = $previewed_value;
  323. }
  324. }
  325. /**
  326. * Run assertions on custom settings.
  327. *
  328. * @see WP_Customize_Setting::preview()
  329. */
  330. function test_preview_custom_type() {
  331. wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) );
  332. $type = 'custom_type';
  333. $post_data_overrides = array(
  334. "unset_{$type}_with_post_value" => "unset_{$type}_without_post_value\\o/",
  335. "set_{$type}_with_post_value" => "set_{$type}_without_post_value\\o/",
  336. );
  337. $_POST['customized'] = wp_slash( wp_json_encode( $post_data_overrides ) );
  338. $this->custom_type_data_saved = array();
  339. $this->custom_type_data_previewed = array();
  340. add_action( "customize_preview_{$type}", array( $this, 'custom_type_preview' ) );
  341. // Custom type not existing and no post value override.
  342. $name = "unset_{$type}_without_post_value";
  343. $default = "default_value_{$name}";
  344. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
  345. // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need.
  346. add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ), 10, 2 );
  347. $this->assertSame( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) );
  348. $this->assertSame( $default, $setting->value() );
  349. $this->assertTrue( $setting->preview() );
  350. $this->assertSame( 1, did_action( "customize_preview_{$setting->id}" ) );
  351. $this->assertSame( 1, did_action( "customize_preview_{$setting->type}" ) );
  352. $this->assertSame( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) ); // Note: for a non-custom type this is $default.
  353. $this->assertSame( $default, $setting->value() ); // Should be same as above.
  354. // Custom type existing and no post value override.
  355. $name = "set_{$type}_without_post_value";
  356. $default = "default_value_{$name}";
  357. $initial_value = "initial_value_{$name}";
  358. $this->custom_type_setter( $name, $initial_value );
  359. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
  360. // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need.
  361. add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ), 10, 2 );
  362. $this->assertSame( $initial_value, $this->custom_type_getter( $name, $this->undefined ) );
  363. $this->assertSame( $initial_value, $setting->value() );
  364. $this->assertFalse( $setting->preview(), "Preview for $setting->id should not apply because existing type without an override." );
  365. $this->assertSame( 0, did_action( "customize_preview_{$setting->id}" ), 'Zero preview actions because initial value is set with no incoming post value, so there is no preview to apply.' );
  366. $this->assertSame( 1, did_action( "customize_preview_{$setting->type}" ) );
  367. $this->assertSame( $initial_value, $this->custom_type_getter( $name, $this->undefined ) ); // Should be same as above.
  368. $this->assertSame( $initial_value, $setting->value() ); // Should be same as above.
  369. // Custom type deferred preview (setting post value after preview ran).
  370. $override_value = "custom_type_value_{$name}_override_deferred_preview";
  371. $this->manager->set_post_value( $setting->id, $override_value );
  372. $this->assertSame( $override_value, $this->custom_type_getter( $name, $this->undefined ) ); // Should be same as above.
  373. $this->assertSame( $override_value, $setting->value() ); // Should be same as above.
  374. // Custom type not existing and with a post value override.
  375. $name = "unset_{$type}_with_post_value";
  376. $default = "default_value_{$name}";
  377. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
  378. // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need.
  379. add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ), 10, 2 );
  380. $this->assertSame( $this->undefined, $this->custom_type_getter( $name, $this->undefined ) );
  381. $this->assertSame( $default, $setting->value() );
  382. $this->assertTrue( $setting->preview() );
  383. $this->assertSame( 1, did_action( "customize_preview_{$setting->id}" ), 'One preview action now because initial value was not set and/or there is no incoming post value, so there is is a preview to apply.' );
  384. $this->assertSame( 3, did_action( "customize_preview_{$setting->type}" ) );
  385. $this->assertSame( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) );
  386. $this->assertSame( $post_data_overrides[ $name ], $setting->value() );
  387. // Custom type not existing and with a post value override.
  388. $name = "set_{$type}_with_post_value";
  389. $default = "default_value_{$name}";
  390. $initial_value = "initial_value_{$name}";
  391. $this->custom_type_setter( $name, $initial_value );
  392. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
  393. // Note: #29316 will allow us to have one filter for all settings of a given type, which is what we need.
  394. add_filter( "customize_value_{$name}", array( $this, 'custom_type_value_filter' ), 10, 2 );
  395. $this->assertSame( $initial_value, $this->custom_type_getter( $name, $this->undefined ) );
  396. $this->assertSame( $initial_value, $setting->value() );
  397. $this->assertTrue( $setting->preview() );
  398. $this->assertSame( 1, did_action( "customize_preview_{$setting->id}" ) );
  399. $this->assertSame( 4, did_action( "customize_preview_{$setting->type}" ) );
  400. $this->assertSame( $post_data_overrides[ $name ], $this->custom_type_getter( $name, $this->undefined ) );
  401. $this->assertSame( $post_data_overrides[ $name ], $setting->value() );
  402. // Custom type that does not handle supplying the post value from the customize_value_{$id_base} filter.
  403. $setting_id = 'custom_without_previewing_value_filter';
  404. $setting = $this->manager->add_setting(
  405. $setting_id,
  406. array(
  407. 'type' => 'custom_preview_test',
  408. 'default' => 123,
  409. 'sanitize_callback' => array( $this->manager->nav_menus, 'intval_base10' ),
  410. )
  411. );
  412. /*
  413. * In #36952 the conditions were such that get_theme_mod() be erroneously used
  414. * to source the root value for a custom multidimensional type.
  415. * Add a theme mod with the same name as the custom setting to test fix.
  416. */
  417. set_theme_mod( $setting_id, 999 );
  418. $this->assertSame( 123, $setting->value() );
  419. $this->manager->set_post_value( $setting_id, '456' );
  420. $setting->preview();
  421. $this->assertSame( 456, $setting->value() );
  422. unset( $this->custom_type_data_previewed, $this->custom_type_data_saved );
  423. remove_theme_mod( $setting_id );
  424. }
  425. /**
  426. * Test specific fix for setting's default value not applying on preview window
  427. *
  428. * @ticket 30988
  429. */
  430. function test_non_posted_setting_applying_default_value_in_preview() {
  431. $type = 'option';
  432. $name = 'unset_option_without_post_value';
  433. $default = "default_value_{$name}";
  434. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
  435. $this->assertSame( $this->undefined, get_option( $name, $this->undefined ) );
  436. $this->assertSame( $default, $setting->value() );
  437. $this->assertTrue( $setting->preview() );
  438. $this->assertSame( $default, get_option( $name, $this->undefined ), sprintf( 'Expected get_option(%s) to return setting default: %s.', $name, $default ) );
  439. $this->assertSame( $default, $setting->value() );
  440. }
  441. /**
  442. * Test setting save method for custom type.
  443. *
  444. * @see WP_Customize_Setting::save()
  445. * @see WP_Customize_Setting::update()
  446. */
  447. function test_update_custom_type() {
  448. $type = 'custom';
  449. $name = 'foo';
  450. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type' ) );
  451. $this->manager->add_setting( $setting );
  452. add_action( 'customize_update_custom', array( $this, 'handle_customize_update_custom_foo_action' ), 10, 2 );
  453. add_action( 'customize_save_foo', array( $this, 'handle_customize_save_custom_foo_action' ), 10, 2 );
  454. // Try saving before value set.
  455. $this->assertTrue( 0 === did_action( 'customize_update_custom' ) );
  456. $this->assertTrue( 0 === did_action( 'customize_save_foo' ) );
  457. $this->assertFalse( $setting->save() );
  458. $this->assertTrue( 0 === did_action( 'customize_update_custom' ) );
  459. $this->assertTrue( 0 === did_action( 'customize_save_foo' ) );
  460. // Try setting post value without user as admin.
  461. $this->manager->set_post_value( $setting->id, 'hello world \\o/' );
  462. $this->assertFalse( $setting->save() );
  463. $this->assertTrue( 0 === did_action( 'customize_update_custom' ) );
  464. $this->assertTrue( 0 === did_action( 'customize_save_foo' ) );
  465. // Satisfy all requirements for save to happen.
  466. wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
  467. $this->assertNotFalse( $setting->save() );
  468. $this->assertTrue( 1 === did_action( 'customize_update_custom' ) );
  469. $this->assertTrue( 1 === did_action( 'customize_save_foo' ) );
  470. }
  471. /**
  472. * Check customize_update_custom action.
  473. *
  474. * @see Tests_WP_Customize_Setting::test_update_custom_type()
  475. * @param mixed $value
  476. * @param WP_Customize_Setting $setting
  477. */
  478. function handle_customize_update_custom_foo_action( $value, $setting = null ) {
  479. $this->assertSame( 'hello world \\o/', $value );
  480. $this->assertInstanceOf( 'WP_Customize_Setting', $setting );
  481. }
  482. /**
  483. * Check customize_save_foo action.
  484. *
  485. * @see Tests_WP_Customize_Setting::test_update_custom_type()
  486. * @param WP_Customize_Setting $setting
  487. */
  488. function handle_customize_save_custom_foo_action( $setting ) {
  489. $this->assertInstanceOf( 'WP_Customize_Setting', $setting );
  490. $this->assertSame( 'custom', $setting->type );
  491. $this->assertSame( 'foo', $setting->id );
  492. }
  493. /**
  494. * Ensure that is_current_blog_previewed returns the expected values.
  495. *
  496. * This is applicable to both single and multisite. This doesn't do switch_to_blog()
  497. *
  498. * @ticket 31428
  499. */
  500. function test_is_current_blog_previewed() {
  501. wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) );
  502. $type = 'option';
  503. $name = 'blogname';
  504. $post_value = __FUNCTION__;
  505. $this->manager->set_post_value( $name, $post_value );
  506. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type' ) );
  507. $this->assertFalse( $setting->is_current_blog_previewed() );
  508. $this->assertTrue( $setting->preview() );
  509. $this->assertTrue( $setting->is_current_blog_previewed() );
  510. $this->assertSame( $post_value, $setting->value() );
  511. $this->assertSame( $post_value, get_option( $name ) );
  512. }
  513. /**
  514. * Ensure that previewing a setting is disabled when the current blog is switched.
  515. *
  516. * @ticket 31428
  517. * @group multisite
  518. * @group ms-required
  519. */
  520. function test_previewing_with_switch_to_blog() {
  521. wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
  522. $type = 'option';
  523. $name = 'blogdescription';
  524. $post_value = __FUNCTION__;
  525. $this->manager->set_post_value( $name, $post_value );
  526. $setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type' ) );
  527. $this->assertFalse( $setting->is_current_blog_previewed() );
  528. $this->assertTrue( $setting->preview() );
  529. $this->assertTrue( $setting->is_current_blog_previewed() );
  530. $blog_id = self::factory()->blog->create();
  531. switch_to_blog( $blog_id );
  532. $this->assertFalse( $setting->is_current_blog_previewed() );
  533. $this->assertNotEquals( $post_value, $setting->value() );
  534. $this->assertNotEquals( $post_value, get_option( $name ) );
  535. restore_current_blog();
  536. }
  537. /**
  538. * @ticket 33499
  539. */
  540. function test_option_autoloading() {
  541. global $wpdb;
  542. wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
  543. $name = 'autoloaded1';
  544. $setting = new WP_Customize_Setting(
  545. $this->manager,
  546. $name,
  547. array(
  548. 'type' => 'option',
  549. )
  550. );
  551. $value = 'value1';
  552. $this->manager->set_post_value( $setting->id, $value );
  553. $setting->save();
  554. $autoload = $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $setting->id ) );
  555. $this->assertSame( 'yes', $autoload );
  556. $this->assertSame( $value, get_option( $name ) );
  557. $name = 'autoloaded2';
  558. $setting = new WP_Customize_Setting(
  559. $this->manager,
  560. $name,
  561. array(
  562. 'type' => 'option',
  563. 'autoload' => true,
  564. )
  565. );
  566. $value = 'value2';
  567. $this->manager->set_post_value( $setting->id, $value );
  568. $setting->save();
  569. $autoload = $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $setting->id ) );
  570. $this->assertSame( 'yes', $autoload );
  571. $this->assertSame( $value, get_option( $name ) );
  572. $name = 'not-autoloaded1';
  573. $setting = new WP_Customize_Setting(
  574. $this->manager,
  575. $name,
  576. array(
  577. 'type' => 'option',
  578. 'autoload' => false,
  579. )
  580. );
  581. $value = 'value3';
  582. $this->manager->set_post_value( $setting->id, $value );
  583. $setting->save();
  584. $autoload = $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $setting->id ) );
  585. $this->assertSame( 'no', $autoload );
  586. $this->assertSame( $value, get_option( $name ) );
  587. $id_base = 'multi-not-autoloaded';
  588. $setting1 = new WP_Customize_Setting(
  589. $this->manager,
  590. $id_base . '[foo]',
  591. array(
  592. 'type' => 'option',
  593. )
  594. );
  595. $setting2 = new WP_Customize_Setting(
  596. $this->manager,
  597. $id_base . '[bar]',
  598. array(
  599. 'type' => 'option',
  600. 'autoload' => false,
  601. )
  602. );
  603. $this->manager->set_post_value( $setting1->id, 'value1' );
  604. $this->manager->set_post_value( $setting2->id, 'value2' );
  605. $setting1->save();
  606. $autoload = $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $id_base ) );
  607. $this->assertSame( 'no', $autoload, 'Even though setting1 did not indicate autoload (thus normally true), since another multidimensional option setting of the base did say autoload=false, it should be autoload=no' );
  608. }
  609. /**
  610. * Test js_value and json methods.
  611. *
  612. * @see WP_Customize_Setting::js_value()
  613. * @see WP_Customize_Setting::json()
  614. */
  615. public function test_js_value() {
  616. $default = "\x00";
  617. $args = array(
  618. 'type' => 'binary',
  619. 'default' => $default,
  620. 'transport' => 'postMessage',
  621. 'dirty' => true,
  622. 'sanitize_js_callback' => array( $this, 'sanitize_js_callback_base64_for_testing' ),
  623. );
  624. $setting = new WP_Customize_Setting( $this->manager, 'name', $args );
  625. $this->assertSame( $default, $setting->value() );
  626. $this->assertSame( base64_encode( $default ), $setting->js_value() );
  627. $exported = $setting->json();
  628. $this->assertArrayHasKey( 'type', $exported );
  629. $this->assertArrayHasKey( 'value', $exported );
  630. $this->assertArrayHasKey( 'transport', $exported );
  631. $this->assertArrayHasKey( 'dirty', $exported );
  632. $this->assertSame( $setting->js_value(), $exported['value'] );
  633. $this->assertSame( $args['type'], $setting->type );
  634. $this->assertSame( $args['transport'], $setting->transport );
  635. $this->assertSame( $args['dirty'], $setting->dirty );
  636. }
  637. /**
  638. * Test validate.
  639. *
  640. * @see WP_Customize_Setting::validate()
  641. */
  642. public function test_validate() {
  643. $setting = new WP_Customize_Setting(
  644. $this->manager,
  645. 'name',
  646. array(
  647. 'type' => 'key',
  648. 'validate_callback' => array( $this, 'filter_validate_for_test_validate' ),
  649. )
  650. );
  651. $validity = $setting->validate( 'BAD!' );
  652. $this->assertInstanceOf( 'WP_Error', $validity );
  653. $this->assertSame( 'invalid_key', $validity->get_error_code() );
  654. }
  655. /**
  656. * Validate callback.
  657. *
  658. * @see Tests_WP_Customize_Setting::test_validate()
  659. *
  660. * @param WP_Error $validity Validity.
  661. * @param string $value Value.
  662. *
  663. * @return WP_Error
  664. */
  665. public function filter_validate_for_test_validate( $validity, $value ) {
  666. $this->assertInstanceOf( 'WP_Error', $validity );
  667. $this->assertInternalType( 'string', $value );
  668. if ( sanitize_key( $value ) !== $value ) {
  669. $validity->add( 'invalid_key', 'Invalid key' );
  670. }
  671. return $validity;
  672. }
  673. /**
  674. * Ensure that WP_Customize_Setting::value() can return a previewed value for aggregated multidimensionals.
  675. *
  676. * @ticket 37294
  677. */
  678. public function test_multidimensional_value_when_previewed() {
  679. wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) );
  680. WP_Customize_Setting::reset_aggregated_multidimensionals();
  681. $initial_value = 456;
  682. set_theme_mod(
  683. 'nav_menu_locations',
  684. array(
  685. 'primary' => $initial_value,
  686. )
  687. );
  688. $setting_id = 'nav_menu_locations[primary]';
  689. $setting = new WP_Customize_Setting( $this->manager, $setting_id );
  690. $this->assertSame( $initial_value, $setting->value() );
  691. $override_value = -123456;
  692. $this->manager->set_post_value( $setting_id, $override_value );
  693. $setting->preview();
  694. $this->assertSame( $override_value, $setting->value() );
  695. }
  696. }