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