class-wp-customize-selective-refresh.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. <?php
  2. /**
  3. * Customize API: WP_Customize_Selective_Refresh class
  4. *
  5. * @package WordPress
  6. * @subpackage Customize
  7. * @since 4.5.0
  8. */
  9. /**
  10. * Core Customizer class for implementing selective refresh.
  11. *
  12. * @since 4.5.0
  13. */
  14. #[AllowDynamicProperties]
  15. final class WP_Customize_Selective_Refresh {
  16. /**
  17. * Query var used in requests to render partials.
  18. *
  19. * @since 4.5.0
  20. */
  21. const RENDER_QUERY_VAR = 'wp_customize_render_partials';
  22. /**
  23. * Customize manager.
  24. *
  25. * @since 4.5.0
  26. * @var WP_Customize_Manager
  27. */
  28. public $manager;
  29. /**
  30. * Registered instances of WP_Customize_Partial.
  31. *
  32. * @since 4.5.0
  33. * @var WP_Customize_Partial[]
  34. */
  35. protected $partials = array();
  36. /**
  37. * Log of errors triggered when partials are rendered.
  38. *
  39. * @since 4.5.0
  40. * @var array
  41. */
  42. protected $triggered_errors = array();
  43. /**
  44. * Keep track of the current partial being rendered.
  45. *
  46. * @since 4.5.0
  47. * @var string|null
  48. */
  49. protected $current_partial_id;
  50. /**
  51. * Plugin bootstrap for Partial Refresh functionality.
  52. *
  53. * @since 4.5.0
  54. *
  55. * @param WP_Customize_Manager $manager Customizer bootstrap instance.
  56. */
  57. public function __construct( WP_Customize_Manager $manager ) {
  58. $this->manager = $manager;
  59. require_once ABSPATH . WPINC . '/customize/class-wp-customize-partial.php';
  60. add_action( 'customize_preview_init', array( $this, 'init_preview' ) );
  61. }
  62. /**
  63. * Retrieves the registered partials.
  64. *
  65. * @since 4.5.0
  66. *
  67. * @return array Partials.
  68. */
  69. public function partials() {
  70. return $this->partials;
  71. }
  72. /**
  73. * Adds a partial.
  74. *
  75. * @since 4.5.0
  76. *
  77. * @see WP_Customize_Partial::__construct()
  78. *
  79. * @param WP_Customize_Partial|string $id Customize Partial object, or Partial ID.
  80. * @param array $args Optional. Array of properties for the new Partials object.
  81. * See WP_Customize_Partial::__construct() for information
  82. * on accepted arguments. Default empty array.
  83. * @return WP_Customize_Partial The instance of the partial that was added.
  84. */
  85. public function add_partial( $id, $args = array() ) {
  86. if ( $id instanceof WP_Customize_Partial ) {
  87. $partial = $id;
  88. } else {
  89. $class = 'WP_Customize_Partial';
  90. /** This filter is documented in wp-includes/customize/class-wp-customize-selective-refresh.php */
  91. $args = apply_filters( 'customize_dynamic_partial_args', $args, $id );
  92. /** This filter is documented in wp-includes/customize/class-wp-customize-selective-refresh.php */
  93. $class = apply_filters( 'customize_dynamic_partial_class', $class, $id, $args );
  94. $partial = new $class( $this, $id, $args );
  95. }
  96. $this->partials[ $partial->id ] = $partial;
  97. return $partial;
  98. }
  99. /**
  100. * Retrieves a partial.
  101. *
  102. * @since 4.5.0
  103. *
  104. * @param string $id Customize Partial ID.
  105. * @return WP_Customize_Partial|null The partial, if set. Otherwise null.
  106. */
  107. public function get_partial( $id ) {
  108. if ( isset( $this->partials[ $id ] ) ) {
  109. return $this->partials[ $id ];
  110. } else {
  111. return null;
  112. }
  113. }
  114. /**
  115. * Removes a partial.
  116. *
  117. * @since 4.5.0
  118. *
  119. * @param string $id Customize Partial ID.
  120. */
  121. public function remove_partial( $id ) {
  122. unset( $this->partials[ $id ] );
  123. }
  124. /**
  125. * Initializes the Customizer preview.
  126. *
  127. * @since 4.5.0
  128. */
  129. public function init_preview() {
  130. add_action( 'template_redirect', array( $this, 'handle_render_partials_request' ) );
  131. add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) );
  132. }
  133. /**
  134. * Enqueues preview scripts.
  135. *
  136. * @since 4.5.0
  137. */
  138. public function enqueue_preview_scripts() {
  139. wp_enqueue_script( 'customize-selective-refresh' );
  140. add_action( 'wp_footer', array( $this, 'export_preview_data' ), 1000 );
  141. }
  142. /**
  143. * Exports data in preview after it has finished rendering so that partials can be added at runtime.
  144. *
  145. * @since 4.5.0
  146. */
  147. public function export_preview_data() {
  148. $partials = array();
  149. foreach ( $this->partials() as $partial ) {
  150. if ( $partial->check_capabilities() ) {
  151. $partials[ $partial->id ] = $partial->json();
  152. }
  153. }
  154. $switched_locale = switch_to_locale( get_user_locale() );
  155. $l10n = array(
  156. 'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
  157. 'clickEditMenu' => __( 'Click to edit this menu.' ),
  158. 'clickEditWidget' => __( 'Click to edit this widget.' ),
  159. 'clickEditTitle' => __( 'Click to edit the site title.' ),
  160. 'clickEditMisc' => __( 'Click to edit this element.' ),
  161. /* translators: %s: document.write() */
  162. 'badDocumentWrite' => sprintf( __( '%s is forbidden' ), 'document.write()' ),
  163. );
  164. if ( $switched_locale ) {
  165. restore_previous_locale();
  166. }
  167. $exports = array(
  168. 'partials' => $partials,
  169. 'renderQueryVar' => self::RENDER_QUERY_VAR,
  170. 'l10n' => $l10n,
  171. );
  172. // Export data to JS.
  173. printf( '<script>var _customizePartialRefreshExports = %s;</script>', wp_json_encode( $exports ) );
  174. }
  175. /**
  176. * Registers dynamically-created partials.
  177. *
  178. * @since 4.5.0
  179. *
  180. * @see WP_Customize_Manager::add_dynamic_settings()
  181. *
  182. * @param string[] $partial_ids Array of the partial IDs to add.
  183. * @return WP_Customize_Partial[] Array of added WP_Customize_Partial instances.
  184. */
  185. public function add_dynamic_partials( $partial_ids ) {
  186. $new_partials = array();
  187. foreach ( $partial_ids as $partial_id ) {
  188. // Skip partials already created.
  189. $partial = $this->get_partial( $partial_id );
  190. if ( $partial ) {
  191. continue;
  192. }
  193. $partial_args = false;
  194. $partial_class = 'WP_Customize_Partial';
  195. /**
  196. * Filters a dynamic partial's constructor arguments.
  197. *
  198. * For a dynamic partial to be registered, this filter must be employed
  199. * to override the default false value with an array of args to pass to
  200. * the WP_Customize_Partial constructor.
  201. *
  202. * @since 4.5.0
  203. *
  204. * @param false|array $partial_args The arguments to the WP_Customize_Partial constructor.
  205. * @param string $partial_id ID for dynamic partial.
  206. */
  207. $partial_args = apply_filters( 'customize_dynamic_partial_args', $partial_args, $partial_id );
  208. if ( false === $partial_args ) {
  209. continue;
  210. }
  211. /**
  212. * Filters the class used to construct partials.
  213. *
  214. * Allow non-statically created partials to be constructed with custom WP_Customize_Partial subclass.
  215. *
  216. * @since 4.5.0
  217. *
  218. * @param string $partial_class WP_Customize_Partial or a subclass.
  219. * @param string $partial_id ID for dynamic partial.
  220. * @param array $partial_args The arguments to the WP_Customize_Partial constructor.
  221. */
  222. $partial_class = apply_filters( 'customize_dynamic_partial_class', $partial_class, $partial_id, $partial_args );
  223. $partial = new $partial_class( $this, $partial_id, $partial_args );
  224. $this->add_partial( $partial );
  225. $new_partials[] = $partial;
  226. }
  227. return $new_partials;
  228. }
  229. /**
  230. * Checks whether the request is for rendering partials.
  231. *
  232. * Note that this will not consider whether the request is authorized or valid,
  233. * just that essentially the route is a match.
  234. *
  235. * @since 4.5.0
  236. *
  237. * @return bool Whether the request is for rendering partials.
  238. */
  239. public function is_render_partials_request() {
  240. return ! empty( $_POST[ self::RENDER_QUERY_VAR ] );
  241. }
  242. /**
  243. * Handles PHP errors triggered during rendering the partials.
  244. *
  245. * These errors will be relayed back to the client in the Ajax response.
  246. *
  247. * @since 4.5.0
  248. *
  249. * @param int $errno Error number.
  250. * @param string $errstr Error string.
  251. * @param string $errfile Error file.
  252. * @param int $errline Error line.
  253. * @return true Always true.
  254. */
  255. public function handle_error( $errno, $errstr, $errfile = null, $errline = null ) {
  256. $this->triggered_errors[] = array(
  257. 'partial' => $this->current_partial_id,
  258. 'error_number' => $errno,
  259. 'error_string' => $errstr,
  260. 'error_file' => $errfile,
  261. 'error_line' => $errline,
  262. );
  263. return true;
  264. }
  265. /**
  266. * Handles the Ajax request to return the rendered partials for the requested placements.
  267. *
  268. * @since 4.5.0
  269. */
  270. public function handle_render_partials_request() {
  271. if ( ! $this->is_render_partials_request() ) {
  272. return;
  273. }
  274. /*
  275. * Note that is_customize_preview() returning true will entail that the
  276. * user passed the 'customize' capability check and the nonce check, since
  277. * WP_Customize_Manager::setup_theme() is where the previewing flag is set.
  278. */
  279. if ( ! is_customize_preview() ) {
  280. wp_send_json_error( 'expected_customize_preview', 403 );
  281. } elseif ( ! isset( $_POST['partials'] ) ) {
  282. wp_send_json_error( 'missing_partials', 400 );
  283. }
  284. // Ensure that doing selective refresh on 404 template doesn't result in fallback rendering behavior (full refreshes).
  285. status_header( 200 );
  286. $partials = json_decode( wp_unslash( $_POST['partials'] ), true );
  287. if ( ! is_array( $partials ) ) {
  288. wp_send_json_error( 'malformed_partials' );
  289. }
  290. $this->add_dynamic_partials( array_keys( $partials ) );
  291. /**
  292. * Fires immediately before partials are rendered.
  293. *
  294. * Plugins may do things like call wp_enqueue_scripts() and gather a list of the scripts
  295. * and styles which may get enqueued in the response.
  296. *
  297. * @since 4.5.0
  298. *
  299. * @param WP_Customize_Selective_Refresh $refresh Selective refresh component.
  300. * @param array $partials Placements' context data for the partials rendered in the request.
  301. * The array is keyed by partial ID, with each item being an array of
  302. * the placements' context data.
  303. */
  304. do_action( 'customize_render_partials_before', $this, $partials );
  305. set_error_handler( array( $this, 'handle_error' ), error_reporting() );
  306. $contents = array();
  307. foreach ( $partials as $partial_id => $container_contexts ) {
  308. $this->current_partial_id = $partial_id;
  309. if ( ! is_array( $container_contexts ) ) {
  310. wp_send_json_error( 'malformed_container_contexts' );
  311. }
  312. $partial = $this->get_partial( $partial_id );
  313. if ( ! $partial || ! $partial->check_capabilities() ) {
  314. $contents[ $partial_id ] = null;
  315. continue;
  316. }
  317. $contents[ $partial_id ] = array();
  318. // @todo The array should include not only the contents, but also whether the container is included?
  319. if ( empty( $container_contexts ) ) {
  320. // Since there are no container contexts, render just once.
  321. $contents[ $partial_id ][] = $partial->render( null );
  322. } else {
  323. foreach ( $container_contexts as $container_context ) {
  324. $contents[ $partial_id ][] = $partial->render( $container_context );
  325. }
  326. }
  327. }
  328. $this->current_partial_id = null;
  329. restore_error_handler();
  330. /**
  331. * Fires immediately after partials are rendered.
  332. *
  333. * Plugins may do things like call wp_footer() to scrape scripts output and return them
  334. * via the {@see 'customize_render_partials_response'} filter.
  335. *
  336. * @since 4.5.0
  337. *
  338. * @param WP_Customize_Selective_Refresh $refresh Selective refresh component.
  339. * @param array $partials Placements' context data for the partials rendered in the request.
  340. * The array is keyed by partial ID, with each item being an array of
  341. * the placements' context data.
  342. */
  343. do_action( 'customize_render_partials_after', $this, $partials );
  344. $response = array(
  345. 'contents' => $contents,
  346. );
  347. if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) {
  348. $response['errors'] = $this->triggered_errors;
  349. }
  350. $setting_validities = $this->manager->validate_setting_values( $this->manager->unsanitized_post_values() );
  351. $exported_setting_validities = array_map( array( $this->manager, 'prepare_setting_validity_for_js' ), $setting_validities );
  352. $response['setting_validities'] = $exported_setting_validities;
  353. /**
  354. * Filters the response from rendering the partials.
  355. *
  356. * Plugins may use this filter to inject `$scripts` and `$styles`, which are dependencies
  357. * for the partials being rendered. The response data will be available to the client via
  358. * the `render-partials-response` JS event, so the client can then inject the scripts and
  359. * styles into the DOM if they have not already been enqueued there.
  360. *
  361. * If plugins do this, they'll need to take care for any scripts that do `document.write()`
  362. * and make sure that these are not injected, or else to override the function to no-op,
  363. * or else the page will be destroyed.
  364. *
  365. * Plugins should be aware that `$scripts` and `$styles` may eventually be included by
  366. * default in the response.
  367. *
  368. * @since 4.5.0
  369. *
  370. * @param array $response {
  371. * Response.
  372. *
  373. * @type array $contents Associative array mapping a partial ID its corresponding array of contents
  374. * for the containers requested.
  375. * @type array $errors List of errors triggered during rendering of partials, if `WP_DEBUG_DISPLAY`
  376. * is enabled.
  377. * }
  378. * @param WP_Customize_Selective_Refresh $refresh Selective refresh component.
  379. * @param array $partials Placements' context data for the partials rendered in the request.
  380. * The array is keyed by partial ID, with each item being an array of
  381. * the placements' context data.
  382. */
  383. $response = apply_filters( 'customize_render_partials_response', $response, $this, $partials );
  384. wp_send_json_success( $response );
  385. }
  386. }