| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 | <?php/** * Customize API: WP_Customize_Selective_Refresh class * * @package WordPress * @subpackage Customize * @since 4.5.0 *//** * Core Customizer class for implementing selective refresh. * * @since 4.5.0 */final class WP_Customize_Selective_Refresh {	/**	 * Query var used in requests to render partials.	 *	 * @since 4.5.0	 */	const RENDER_QUERY_VAR = 'wp_customize_render_partials';	/**	 * Customize manager.	 *	 * @since 4.5.0	 * @var WP_Customize_Manager	 */	public $manager;	/**	 * Registered instances of WP_Customize_Partial.	 *	 * @since 4.5.0	 * @var WP_Customize_Partial[]	 */	protected $partials = array();	/**	 * Log of errors triggered when partials are rendered.	 *	 * @since 4.5.0	 * @var array	 */	protected $triggered_errors = array();	/**	 * Keep track of the current partial being rendered.	 *	 * @since 4.5.0	 * @var string|null	 */	protected $current_partial_id;	/**	 * Plugin bootstrap for Partial Refresh functionality.	 *	 * @since 4.5.0	 *	 * @param WP_Customize_Manager $manager Manager instance.	 */	public function __construct( WP_Customize_Manager $manager ) {		$this->manager = $manager;		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-partial.php' );		add_action( 'customize_preview_init', array( $this, 'init_preview' ) );	}	/**	 * Retrieves the registered partials.	 *	 * @since 4.5.0	 *	 * @return array Partials.	 */	public function partials() {		return $this->partials;	}	/**	 * Adds a partial.	 *	 * @since 4.5.0	 *	 * @param WP_Customize_Partial|string $id   Customize Partial object, or Panel ID.	 * @param array                       $args {	 *  Optional. Array of properties for the new Partials object. Default empty array.	 *	 *  @type string   $type                  Type of the partial to be created.	 *  @type string   $selector              The jQuery selector to find the container element for the partial, that is, a partial's placement.	 *  @type array    $settings              IDs for settings tied to the partial.	 *  @type string   $primary_setting       The ID for the setting that this partial is primarily responsible for	 *                                        rendering. If not supplied, it will default to the ID of the first setting.	 *  @type string   $capability            Capability required to edit this partial.	 *                                        Normally this is empty and the capability is derived from the capabilities	 *                                        of the associated `$settings`.	 *  @type callable $render_callback       Render callback.	 *                                        Callback is called with one argument, the instance of WP_Customize_Partial.	 *                                        The callback can either echo the partial or return the partial as a string,	 *                                        or return false if error.	 *  @type bool     $container_inclusive   Whether the container element is included in the partial, or if only	 *                                        the contents are rendered.	 *  @type bool     $fallback_refresh      Whether to refresh the entire preview in case a partial cannot be refreshed.	 *                                        A partial render is considered a failure if the render_callback returns	 *                                        false.	 * }	 * @return WP_Customize_Partial             The instance of the panel that was added.	 */	public function add_partial( $id, $args = array() ) {		if ( $id instanceof WP_Customize_Partial ) {			$partial = $id;		} else {			$class = 'WP_Customize_Partial';			/** This filter is documented in wp-includes/customize/class-wp-customize-selective-refresh.php */			$args = apply_filters( 'customize_dynamic_partial_args', $args, $id );			/** This filter is documented in wp-includes/customize/class-wp-customize-selective-refresh.php */			$class = apply_filters( 'customize_dynamic_partial_class', $class, $id, $args );			$partial = new $class( $this, $id, $args );		}		$this->partials[ $partial->id ] = $partial;		return $partial;	}	/**	 * Retrieves a partial.	 *	 * @since 4.5.0	 *	 * @param string $id Customize Partial ID.	 * @return WP_Customize_Partial|null The partial, if set. Otherwise null.	 */	public function get_partial( $id ) {		if ( isset( $this->partials[ $id ] ) ) {			return $this->partials[ $id ];		} else {			return null;		}	}	/**	 * Removes a partial.	 *	 * @since 4.5.0	 *	 * @param string $id Customize Partial ID.	 */	public function remove_partial( $id ) {		unset( $this->partials[ $id ] );	}	/**	 * Initializes the Customizer preview.	 *	 * @since 4.5.0	 */	public function init_preview() {		add_action( 'template_redirect', array( $this, 'handle_render_partials_request' ) );		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) );	}	/**	 * Enqueues preview scripts.	 *	 * @since 4.5.0	 */	public function enqueue_preview_scripts() {		wp_enqueue_script( 'customize-selective-refresh' );		add_action( 'wp_footer', array( $this, 'export_preview_data' ), 1000 );	}	/**	 * Exports data in preview after it has finished rendering so that partials can be added at runtime.	 *	 * @since 4.5.0	 */	public function export_preview_data() {		$partials = array();		foreach ( $this->partials() as $partial ) {			if ( $partial->check_capabilities() ) {				$partials[ $partial->id ] = $partial->json();			}		}		$switched_locale = switch_to_locale( get_user_locale() );		$l10n            = array(			'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),			'clickEditMenu'    => __( 'Click to edit this menu.' ),			'clickEditWidget'  => __( 'Click to edit this widget.' ),			'clickEditTitle'   => __( 'Click to edit the site title.' ),			'clickEditMisc'    => __( 'Click to edit this element.' ),			/* translators: %s: document.write() */			'badDocumentWrite' => sprintf( __( '%s is forbidden' ), 'document.write()' ),		);		if ( $switched_locale ) {			restore_previous_locale();		}		$exports = array(			'partials'       => $partials,			'renderQueryVar' => self::RENDER_QUERY_VAR,			'l10n'           => $l10n,		);		// Export data to JS.		echo sprintf( '<script>var _customizePartialRefreshExports = %s;</script>', wp_json_encode( $exports ) );	}	/**	 * Registers dynamically-created partials.	 *	 * @since 4.5.0	 *	 * @see WP_Customize_Manager::add_dynamic_settings()	 *	 * @param string[] $partial_ids Array of the partial IDs to add.	 * @return WP_Customize_Partial[] Array of added WP_Customize_Partial instances.	 */	public function add_dynamic_partials( $partial_ids ) {		$new_partials = array();		foreach ( $partial_ids as $partial_id ) {			// Skip partials already created.			$partial = $this->get_partial( $partial_id );			if ( $partial ) {				continue;			}			$partial_args  = false;			$partial_class = 'WP_Customize_Partial';			/**			 * Filters a dynamic partial's constructor arguments.			 *			 * For a dynamic partial to be registered, this filter must be employed			 * to override the default false value with an array of args to pass to			 * the WP_Customize_Partial constructor.			 *			 * @since 4.5.0			 *			 * @param false|array $partial_args The arguments to the WP_Customize_Partial constructor.			 * @param string      $partial_id   ID for dynamic partial.			 */			$partial_args = apply_filters( 'customize_dynamic_partial_args', $partial_args, $partial_id );			if ( false === $partial_args ) {				continue;			}			/**			 * Filters the class used to construct partials.			 *			 * Allow non-statically created partials to be constructed with custom WP_Customize_Partial subclass.			 *			 * @since 4.5.0			 *			 * @param string $partial_class WP_Customize_Partial or a subclass.			 * @param string $partial_id    ID for dynamic partial.			 * @param array  $partial_args  The arguments to the WP_Customize_Partial constructor.			 */			$partial_class = apply_filters( 'customize_dynamic_partial_class', $partial_class, $partial_id, $partial_args );			$partial = new $partial_class( $this, $partial_id, $partial_args );			$this->add_partial( $partial );			$new_partials[] = $partial;		}		return $new_partials;	}	/**	 * Checks whether the request is for rendering partials.	 *	 * Note that this will not consider whether the request is authorized or valid,	 * just that essentially the route is a match.	 *	 * @since 4.5.0	 *	 * @return bool Whether the request is for rendering partials.	 */	public function is_render_partials_request() {		return ! empty( $_POST[ self::RENDER_QUERY_VAR ] );	}	/**	 * Handles PHP errors triggered during rendering the partials.	 *	 * These errors will be relayed back to the client in the Ajax response.	 *	 * @since 4.5.0	 *	 * @param int    $errno   Error number.	 * @param string $errstr  Error string.	 * @param string $errfile Error file.	 * @param string $errline Error line.	 * @return true Always true.	 */	public function handle_error( $errno, $errstr, $errfile = null, $errline = null ) {		$this->triggered_errors[] = array(			'partial'      => $this->current_partial_id,			'error_number' => $errno,			'error_string' => $errstr,			'error_file'   => $errfile,			'error_line'   => $errline,		);		return true;	}	/**	 * Handles the Ajax request to return the rendered partials for the requested placements.	 *	 * @since 4.5.0	 */	public function handle_render_partials_request() {		if ( ! $this->is_render_partials_request() ) {			return;		}		/*		 * Note that is_customize_preview() returning true will entail that the		 * user passed the 'customize' capability check and the nonce check, since		 * WP_Customize_Manager::setup_theme() is where the previewing flag is set.		 */		if ( ! is_customize_preview() ) {			wp_send_json_error( 'expected_customize_preview', 403 );		} elseif ( ! isset( $_POST['partials'] ) ) {			wp_send_json_error( 'missing_partials', 400 );		}		// Ensure that doing selective refresh on 404 template doesn't result in fallback rendering behavior (full refreshes).		status_header( 200 );		$partials = json_decode( wp_unslash( $_POST['partials'] ), true );		if ( ! is_array( $partials ) ) {			wp_send_json_error( 'malformed_partials' );		}		$this->add_dynamic_partials( array_keys( $partials ) );		/**		 * Fires immediately before partials are rendered.		 *		 * Plugins may do things like call wp_enqueue_scripts() and gather a list of the scripts		 * and styles which may get enqueued in the response.		 *		 * @since 4.5.0		 *		 * @param WP_Customize_Selective_Refresh $this     Selective refresh component.		 * @param array                          $partials Placements' context data for the partials rendered in the request.		 *                                                 The array is keyed by partial ID, with each item being an array of		 *                                                 the placements' context data.		 */		do_action( 'customize_render_partials_before', $this, $partials );		set_error_handler( array( $this, 'handle_error' ), error_reporting() );		$contents = array();		foreach ( $partials as $partial_id => $container_contexts ) {			$this->current_partial_id = $partial_id;			if ( ! is_array( $container_contexts ) ) {				wp_send_json_error( 'malformed_container_contexts' );			}			$partial = $this->get_partial( $partial_id );			if ( ! $partial || ! $partial->check_capabilities() ) {				$contents[ $partial_id ] = null;				continue;			}			$contents[ $partial_id ] = array();			// @todo The array should include not only the contents, but also whether the container is included?			if ( empty( $container_contexts ) ) {				// Since there are no container contexts, render just once.				$contents[ $partial_id ][] = $partial->render( null );			} else {				foreach ( $container_contexts as $container_context ) {					$contents[ $partial_id ][] = $partial->render( $container_context );				}			}		}		$this->current_partial_id = null;		restore_error_handler();		/**		 * Fires immediately after partials are rendered.		 *		 * Plugins may do things like call wp_footer() to scrape scripts output and return them		 * via the {@see 'customize_render_partials_response'} filter.		 *		 * @since 4.5.0		 *		 * @param WP_Customize_Selective_Refresh $this     Selective refresh component.		 * @param array                          $partials Placements' context data for the partials rendered in the request.		 *                                                 The array is keyed by partial ID, with each item being an array of		 *                                                 the placements' context data.		 */		do_action( 'customize_render_partials_after', $this, $partials );		$response = array(			'contents' => $contents,		);		if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) {			$response['errors'] = $this->triggered_errors;		}		$setting_validities             = $this->manager->validate_setting_values( $this->manager->unsanitized_post_values() );		$exported_setting_validities    = array_map( array( $this->manager, 'prepare_setting_validity_for_js' ), $setting_validities );		$response['setting_validities'] = $exported_setting_validities;		/**		 * Filters the response from rendering the partials.		 *		 * Plugins may use this filter to inject `$scripts` and `$styles`, which are dependencies		 * for the partials being rendered. The response data will be available to the client via		 * the `render-partials-response` JS event, so the client can then inject the scripts and		 * styles into the DOM if they have not already been enqueued there.		 *		 * If plugins do this, they'll need to take care for any scripts that do `document.write()`		 * and make sure that these are not injected, or else to override the function to no-op,		 * or else the page will be destroyed.		 *		 * Plugins should be aware that `$scripts` and `$styles` may eventually be included by		 * default in the response.		 *		 * @since 4.5.0		 *		 * @param array $response {		 *     Response.		 *		 *     @type array $contents Associative array mapping a partial ID its corresponding array of contents		 *                           for the containers requested.		 *     @type array $errors   List of errors triggered during rendering of partials, if `WP_DEBUG_DISPLAY`		 *                           is enabled.		 * }		 * @param WP_Customize_Selective_Refresh $this     Selective refresh component.		 * @param array                          $partials Placements' context data for the partials rendered in the request.		 *                                                 The array is keyed by partial ID, with each item being an array of		 *                                                 the placements' context data.		 */		$response = apply_filters( 'customize_render_partials_response', $response, $this, $partials );		wp_send_json_success( $response );	}}
 |