3 * Customize API: WP_Customize_Selective_Refresh class
6 * @subpackage Customize
11 * Core Customizer class for implementing selective refresh.
15 final class WP_Customize_Selective_Refresh {
18 * Query var used in requests to render partials.
22 const RENDER_QUERY_VAR = 'wp_customize_render_partials';
29 * @var WP_Customize_Manager
34 * Registered instances of WP_Customize_Partial.
38 * @var WP_Customize_Partial[]
40 protected $partials = array();
43 * Log of errors triggered when partials are rendered.
49 protected $triggered_errors = array();
52 * Keep track of the current partial being rendered.
58 protected $current_partial_id;
61 * Plugin bootstrap for Partial Refresh functionality.
66 * @param WP_Customize_Manager $manager Manager instance.
68 public function __construct( WP_Customize_Manager $manager ) {
69 $this->manager = $manager;
70 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-partial.php' );
72 add_action( 'customize_preview_init', array( $this, 'init_preview' ) );
76 * Retrieves the registered partials.
81 * @return array Partials.
83 public function partials() {
84 return $this->partials;
93 * @param WP_Customize_Partial|string $id Customize Partial object, or Panel ID.
94 * @param array $args Optional. Partial arguments. Default empty array.
95 * @return WP_Customize_Partial The instance of the panel that was added.
97 public function add_partial( $id, $args = array() ) {
98 if ( $id instanceof WP_Customize_Partial ) {
101 $class = 'WP_Customize_Partial';
103 /** This filter (will be) documented in wp-includes/class-wp-customize-manager.php */
104 $args = apply_filters( 'customize_dynamic_partial_args', $args, $id );
106 /** This filter (will be) documented in wp-includes/class-wp-customize-manager.php */
107 $class = apply_filters( 'customize_dynamic_partial_class', $class, $id, $args );
109 $partial = new $class( $this, $id, $args );
112 $this->partials[ $partial->id ] = $partial;
117 * Retrieves a partial.
122 * @param string $id Customize Partial ID.
123 * @return WP_Customize_Partial|null The partial, if set. Otherwise null.
125 public function get_partial( $id ) {
126 if ( isset( $this->partials[ $id ] ) ) {
127 return $this->partials[ $id ];
139 * @param string $id Customize Partial ID.
141 public function remove_partial( $id ) {
142 unset( $this->partials[ $id ] );
146 * Initializes the Customizer preview.
151 public function init_preview() {
152 add_action( 'template_redirect', array( $this, 'handle_render_partials_request' ) );
153 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) );
157 * Enqueues preview scripts.
162 public function enqueue_preview_scripts() {
163 wp_enqueue_script( 'customize-selective-refresh' );
164 add_action( 'wp_footer', array( $this, 'export_preview_data' ), 1000 );
168 * Exports data in preview after it has finished rendering so that partials can be added at runtime.
173 public function export_preview_data() {
176 foreach ( $this->partials() as $partial ) {
177 if ( $partial->check_capabilities() ) {
178 $partials[ $partial->id ] = $partial->json();
183 'partials' => $partials,
184 'renderQueryVar' => self::RENDER_QUERY_VAR,
186 'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
187 'clickEditMenu' => __( 'Click to edit this menu.' ),
188 'clickEditWidget' => __( 'Click to edit this widget.' ),
189 'clickEditTitle' => __( 'Click to edit the site title.' ),
190 'clickEditMisc' => __( 'Click to edit this element.' ),
191 /* translators: %s: document.write() */
192 'badDocumentWrite' => sprintf( __( '%s is forbidden' ), 'document.write()' ),
196 // Export data to JS.
197 echo sprintf( '<script>var _customizePartialRefreshExports = %s;</script>', wp_json_encode( $exports ) );
201 * Registers dynamically-created partials.
206 * @see WP_Customize_Manager::add_dynamic_settings()
208 * @param array $partial_ids The partial ID to add.
209 * @return array Added WP_Customize_Partial instances.
211 public function add_dynamic_partials( $partial_ids ) {
212 $new_partials = array();
214 foreach ( $partial_ids as $partial_id ) {
216 // Skip partials already created.
217 $partial = $this->get_partial( $partial_id );
222 $partial_args = false;
223 $partial_class = 'WP_Customize_Partial';
226 * Filters a dynamic partial's constructor arguments.
228 * For a dynamic partial to be registered, this filter must be employed
229 * to override the default false value with an array of args to pass to
230 * the WP_Customize_Partial constructor.
234 * @param false|array $partial_args The arguments to the WP_Customize_Partial constructor.
235 * @param string $partial_id ID for dynamic partial.
237 $partial_args = apply_filters( 'customize_dynamic_partial_args', $partial_args, $partial_id );
238 if ( false === $partial_args ) {
243 * Filters the class used to construct partials.
245 * Allow non-statically created partials to be constructed with custom WP_Customize_Partial subclass.
249 * @param string $partial_class WP_Customize_Partial or a subclass.
250 * @param string $partial_id ID for dynamic partial.
251 * @param array $partial_args The arguments to the WP_Customize_Partial constructor.
253 $partial_class = apply_filters( 'customize_dynamic_partial_class', $partial_class, $partial_id, $partial_args );
255 $partial = new $partial_class( $this, $partial_id, $partial_args );
257 $this->add_partial( $partial );
258 $new_partials[] = $partial;
260 return $new_partials;
264 * Checks whether the request is for rendering partials.
266 * Note that this will not consider whether the request is authorized or valid,
267 * just that essentially the route is a match.
272 * @return bool Whether the request is for rendering partials.
274 public function is_render_partials_request() {
275 return ! empty( $_POST[ self::RENDER_QUERY_VAR ] );
279 * Handles PHP errors triggered during rendering the partials.
281 * These errors will be relayed back to the client in the Ajax response.
286 * @param int $errno Error number.
287 * @param string $errstr Error string.
288 * @param string $errfile Error file.
289 * @param string $errline Error line.
290 * @return true Always true.
292 public function handle_error( $errno, $errstr, $errfile = null, $errline = null ) {
293 $this->triggered_errors[] = array(
294 'partial' => $this->current_partial_id,
295 'error_number' => $errno,
296 'error_string' => $errstr,
297 'error_file' => $errfile,
298 'error_line' => $errline,
304 * Handles the Ajax request to return the rendered partials for the requested placements.
309 public function handle_render_partials_request() {
310 if ( ! $this->is_render_partials_request() ) {
315 * Note that is_customize_preview() returning true will entail that the
316 * user passed the 'customize' capability check and the nonce check, since
317 * WP_Customize_Manager::setup_theme() is where the previewing flag is set.
319 if ( ! is_customize_preview() ) {
320 wp_send_json_error( 'expected_customize_preview', 403 );
321 } else if ( ! isset( $_POST['partials'] ) ) {
322 wp_send_json_error( 'missing_partials', 400 );
325 $partials = json_decode( wp_unslash( $_POST['partials'] ), true );
327 if ( ! is_array( $partials ) ) {
328 wp_send_json_error( 'malformed_partials' );
331 $this->add_dynamic_partials( array_keys( $partials ) );
334 * Fires immediately before partials are rendered.
336 * Plugins may do things like call wp_enqueue_scripts() and gather a list of the scripts
337 * and styles which may get enqueued in the response.
341 * @param WP_Customize_Selective_Refresh $this Selective refresh component.
342 * @param array $partials Placements' context data for the partials rendered in the request.
343 * The array is keyed by partial ID, with each item being an array of
344 * the placements' context data.
346 do_action( 'customize_render_partials_before', $this, $partials );
348 set_error_handler( array( $this, 'handle_error' ), error_reporting() );
352 foreach ( $partials as $partial_id => $container_contexts ) {
353 $this->current_partial_id = $partial_id;
355 if ( ! is_array( $container_contexts ) ) {
356 wp_send_json_error( 'malformed_container_contexts' );
359 $partial = $this->get_partial( $partial_id );
361 if ( ! $partial || ! $partial->check_capabilities() ) {
362 $contents[ $partial_id ] = null;
366 $contents[ $partial_id ] = array();
368 // @todo The array should include not only the contents, but also whether the container is included?
369 if ( empty( $container_contexts ) ) {
370 // Since there are no container contexts, render just once.
371 $contents[ $partial_id ][] = $partial->render( null );
373 foreach ( $container_contexts as $container_context ) {
374 $contents[ $partial_id ][] = $partial->render( $container_context );
378 $this->current_partial_id = null;
380 restore_error_handler();
383 * Fires immediately after partials are rendered.
385 * Plugins may do things like call wp_footer() to scrape scripts output and return them
386 * via the {@see 'customize_render_partials_response'} filter.
390 * @param WP_Customize_Selective_Refresh $this Selective refresh component.
391 * @param array $partials Placements' context data for the partials rendered in the request.
392 * The array is keyed by partial ID, with each item being an array of
393 * the placements' context data.
395 do_action( 'customize_render_partials_after', $this, $partials );
398 'contents' => $contents,
401 if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) {
402 $response['errors'] = $this->triggered_errors;
405 $setting_validities = $this->manager->validate_setting_values( $this->manager->unsanitized_post_values() );
406 $exported_setting_validities = array_map( array( $this->manager, 'prepare_setting_validity_for_js' ), $setting_validities );
407 $response['setting_validities'] = $exported_setting_validities;
410 * Filters the response from rendering the partials.
412 * Plugins may use this filter to inject `$scripts` and `$styles`, which are dependencies
413 * for the partials being rendered. The response data will be available to the client via
414 * the `render-partials-response` JS event, so the client can then inject the scripts and
415 * styles into the DOM if they have not already been enqueued there.
417 * If plugins do this, they'll need to take care for any scripts that do `document.write()`
418 * and make sure that these are not injected, or else to override the function to no-op,
419 * or else the page will be destroyed.
421 * Plugins should be aware that `$scripts` and `$styles` may eventually be included by
422 * default in the response.
426 * @param array $response {
429 * @type array $contents Associative array mapping a partial ID its corresponding array of contents
430 * for the containers requested.
431 * @type array $errors List of errors triggered during rendering of partials, if `WP_DEBUG_DISPLAY`
434 * @param WP_Customize_Selective_Refresh $this Selective refresh component.
435 * @param array $partials Placements' context data for the partials rendered in the request.
436 * The array is keyed by partial ID, with each item being an array of
437 * the placements' context data.
439 $response = apply_filters( 'customize_render_partials_response', $response, $this, $partials );
441 wp_send_json_success( $response );