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