Wordpress 4.6
[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                 $exports = array(
183                         'partials'       => $partials,
184                         'renderQueryVar' => self::RENDER_QUERY_VAR,
185                         'l10n'           => array(
186                                 'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
187                                 /* translators: %s: document.write() */
188                                 'badDocumentWrite' => sprintf( __( '%s is forbidden' ), 'document.write()' ),
189                         ),
190                 );
191
192                 // Export data to JS.
193                 echo sprintf( '<script>var _customizePartialRefreshExports = %s;</script>', wp_json_encode( $exports ) );
194         }
195
196         /**
197          * Registers dynamically-created partials.
198          *
199          * @since 4.5.0
200          * @access public
201          *
202          * @see WP_Customize_Manager::add_dynamic_settings()
203          *
204          * @param array $partial_ids The partial ID to add.
205          * @return array Added WP_Customize_Partial instances.
206          */
207         public function add_dynamic_partials( $partial_ids ) {
208                 $new_partials = array();
209
210                 foreach ( $partial_ids as $partial_id ) {
211
212                         // Skip partials already created.
213                         $partial = $this->get_partial( $partial_id );
214                         if ( $partial ) {
215                                 continue;
216                         }
217
218                         $partial_args = false;
219                         $partial_class = 'WP_Customize_Partial';
220
221                         /**
222                          * Filters a dynamic partial's constructor arguments.
223                          *
224                          * For a dynamic partial to be registered, this filter must be employed
225                          * to override the default false value with an array of args to pass to
226                          * the WP_Customize_Partial constructor.
227                          *
228                          * @since 4.5.0
229                          *
230                          * @param false|array $partial_args The arguments to the WP_Customize_Partial constructor.
231                          * @param string      $partial_id   ID for dynamic partial.
232                          */
233                         $partial_args = apply_filters( 'customize_dynamic_partial_args', $partial_args, $partial_id );
234                         if ( false === $partial_args ) {
235                                 continue;
236                         }
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
251                         $partial = new $partial_class( $this, $partial_id, $partial_args );
252
253                         $this->add_partial( $partial );
254                         $new_partials[] = $partial;
255                 }
256                 return $new_partials;
257         }
258
259         /**
260          * Checks whether the request is for rendering partials.
261          *
262          * Note that this will not consider whether the request is authorized or valid,
263          * just that essentially the route is a match.
264          *
265          * @since 4.5.0
266          * @access public
267          *
268          * @return bool Whether the request is for rendering partials.
269          */
270         public function is_render_partials_request() {
271                 return ! empty( $_POST[ self::RENDER_QUERY_VAR ] );
272         }
273
274         /**
275          * Handles PHP errors triggered during rendering the partials.
276          *
277          * These errors will be relayed back to the client in the Ajax response.
278          *
279          * @since 4.5.0
280          * @access private
281          *
282          * @param int    $errno   Error number.
283          * @param string $errstr  Error string.
284          * @param string $errfile Error file.
285          * @param string $errline Error line.
286          * @return true Always true.
287          */
288         public function handle_error( $errno, $errstr, $errfile = null, $errline = null ) {
289                 $this->triggered_errors[] = array(
290                         'partial'      => $this->current_partial_id,
291                         'error_number' => $errno,
292                         'error_string' => $errstr,
293                         'error_file'   => $errfile,
294                         'error_line'   => $errline,
295                 );
296                 return true;
297         }
298
299         /**
300          * Handles the Ajax request to return the rendered partials for the requested placements.
301          *
302          * @since 4.5.0
303          * @access public
304          */
305         public function handle_render_partials_request() {
306                 if ( ! $this->is_render_partials_request() ) {
307                         return;
308                 }
309
310                 $this->manager->remove_preview_signature();
311
312                 /*
313                  * Note that is_customize_preview() returning true will entail that the
314                  * user passed the 'customize' capability check and the nonce check, since
315                  * WP_Customize_Manager::setup_theme() is where the previewing flag is set.
316                  */
317                 if ( ! is_customize_preview() ) {
318                         status_header( 403 );
319                         wp_send_json_error( 'expected_customize_preview' );
320                 } else if ( ! isset( $_POST['partials'] ) ) {
321                         status_header( 400 );
322                         wp_send_json_error( 'missing_partials' );
323                 }
324
325                 $partials = json_decode( wp_unslash( $_POST['partials'] ), true );
326
327                 if ( ! is_array( $partials ) ) {
328                         wp_send_json_error( 'malformed_partials' );
329                 }
330
331                 $this->add_dynamic_partials( array_keys( $partials ) );
332
333                 /**
334                  * Fires immediately before partials are rendered.
335                  *
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.
338                  *
339                  * @since 4.5.0
340                  *
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.
345                  */
346                 do_action( 'customize_render_partials_before', $this, $partials );
347
348                 set_error_handler( array( $this, 'handle_error' ), error_reporting() );
349
350                 $contents = array();
351
352                 foreach ( $partials as $partial_id => $container_contexts ) {
353                         $this->current_partial_id = $partial_id;
354
355                         if ( ! is_array( $container_contexts ) ) {
356                                 wp_send_json_error( 'malformed_container_contexts' );
357                         }
358
359                         $partial = $this->get_partial( $partial_id );
360
361                         if ( ! $partial || ! $partial->check_capabilities() ) {
362                                 $contents[ $partial_id ] = null;
363                                 continue;
364                         }
365
366                         $contents[ $partial_id ] = array();
367
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 );
372                         } else {
373                                 foreach ( $container_contexts as $container_context ) {
374                                         $contents[ $partial_id ][] = $partial->render( $container_context );
375                                 }
376                         }
377                 }
378                 $this->current_partial_id = null;
379
380                 restore_error_handler();
381
382                 /**
383                  * Fires immediately after partials are rendered.
384                  *
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.
387                  *
388                  * @since 4.5.0
389                  *
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.
394                  */
395                 do_action( 'customize_render_partials_after', $this, $partials );
396
397                 $response = array(
398                         'contents' => $contents,
399                 );
400
401                 if ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY ) {
402                         $response['errors'] = $this->triggered_errors;
403                 }
404
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;
408
409                 /**
410                  * Filters the response from rendering the partials.
411                  *
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.
416                  *
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.
420                  *
421                  * Plugins should be aware that `$scripts` and `$styles` may eventually be included by
422                  * default in the response.
423                  *
424                  * @since 4.5.0
425                  *
426                  * @param array $response {
427                  *     Response.
428                  *
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`
432                  *                           is enabled.
433                  * }
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.
438                  */
439                 $response = apply_filters( 'customize_render_partials_response', $response, $this, $partials );
440
441                 wp_send_json_success( $response );
442         }
443 }