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

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