|
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 } |