22 */ |
22 */ |
23 final class WP_Customize_Manager { |
23 final class WP_Customize_Manager { |
24 /** |
24 /** |
25 * An instance of the theme being previewed. |
25 * An instance of the theme being previewed. |
26 * |
26 * |
|
27 * @since 3.4.0 |
27 * @var WP_Theme |
28 * @var WP_Theme |
28 */ |
29 */ |
29 protected $theme; |
30 protected $theme; |
30 |
31 |
31 /** |
32 /** |
32 * The directory name of the previously active theme (within the theme_root). |
33 * The directory name of the previously active theme (within the theme_root). |
33 * |
34 * |
|
35 * @since 3.4.0 |
34 * @var string |
36 * @var string |
35 */ |
37 */ |
36 protected $original_stylesheet; |
38 protected $original_stylesheet; |
37 |
39 |
38 /** |
40 /** |
39 * Whether this is a Customizer pageload. |
41 * Whether this is a Customizer pageload. |
40 * |
42 * |
41 * @var boolean |
43 * @since 3.4.0 |
|
44 * @var bool |
42 */ |
45 */ |
43 protected $previewing = false; |
46 protected $previewing = false; |
44 |
47 |
45 /** |
48 /** |
46 * Methods and properties deailing with managing widgets in the Customizer. |
49 * Methods and properties dealing with managing widgets in the Customizer. |
47 * |
50 * |
|
51 * @since 3.9.0 |
48 * @var WP_Customize_Widgets |
52 * @var WP_Customize_Widgets |
49 */ |
53 */ |
50 public $widgets; |
54 public $widgets; |
51 |
55 |
52 protected $settings = array(); |
56 /** |
|
57 * Methods and properties dealing with managing nav menus in the Customizer. |
|
58 * |
|
59 * @since 4.3.0 |
|
60 * @var WP_Customize_Nav_Menus |
|
61 */ |
|
62 public $nav_menus; |
|
63 |
|
64 /** |
|
65 * Methods and properties dealing with selective refresh in the Customizer preview. |
|
66 * |
|
67 * @since 4.5.0 |
|
68 * @var WP_Customize_Selective_Refresh |
|
69 */ |
|
70 public $selective_refresh; |
|
71 |
|
72 /** |
|
73 * Registered instances of WP_Customize_Setting. |
|
74 * |
|
75 * @since 3.4.0 |
|
76 * @var array |
|
77 */ |
|
78 protected $settings = array(); |
|
79 |
|
80 /** |
|
81 * Sorted top-level instances of WP_Customize_Panel and WP_Customize_Section. |
|
82 * |
|
83 * @since 4.0.0 |
|
84 * @var array |
|
85 */ |
53 protected $containers = array(); |
86 protected $containers = array(); |
54 protected $panels = array(); |
87 |
55 protected $sections = array(); |
88 /** |
56 protected $controls = array(); |
89 * Registered instances of WP_Customize_Panel. |
57 |
90 * |
58 protected $nonce_tick; |
91 * @since 4.0.0 |
59 |
92 * @var array |
60 protected $customized; |
93 */ |
61 |
94 protected $panels = array(); |
62 /** |
95 |
63 * Controls that may be rendered from JS templates. |
96 /** |
|
97 * List of core components. |
|
98 * |
|
99 * @since 4.5.0 |
|
100 * @var array |
|
101 */ |
|
102 protected $components = array( 'widgets', 'nav_menus' ); |
|
103 |
|
104 /** |
|
105 * Registered instances of WP_Customize_Section. |
|
106 * |
|
107 * @since 3.4.0 |
|
108 * @var array |
|
109 */ |
|
110 protected $sections = array(); |
|
111 |
|
112 /** |
|
113 * Registered instances of WP_Customize_Control. |
|
114 * |
|
115 * @since 3.4.0 |
|
116 * @var array |
|
117 */ |
|
118 protected $controls = array(); |
|
119 |
|
120 /** |
|
121 * Panel types that may be rendered from JS templates. |
|
122 * |
|
123 * @since 4.3.0 |
|
124 * @var array |
|
125 */ |
|
126 protected $registered_panel_types = array(); |
|
127 |
|
128 /** |
|
129 * Section types that may be rendered from JS templates. |
|
130 * |
|
131 * @since 4.3.0 |
|
132 * @var array |
|
133 */ |
|
134 protected $registered_section_types = array(); |
|
135 |
|
136 /** |
|
137 * Control types that may be rendered from JS templates. |
64 * |
138 * |
65 * @since 4.1.0 |
139 * @since 4.1.0 |
66 * @access protected |
|
67 * @var array |
140 * @var array |
68 */ |
141 */ |
69 protected $registered_control_types = array(); |
142 protected $registered_control_types = array(); |
70 |
143 |
71 /** |
144 /** |
|
145 * Initial URL being previewed. |
|
146 * |
|
147 * @since 4.4.0 |
|
148 * @var string |
|
149 */ |
|
150 protected $preview_url; |
|
151 |
|
152 /** |
|
153 * URL to link the user to when closing the Customizer. |
|
154 * |
|
155 * @since 4.4.0 |
|
156 * @var string |
|
157 */ |
|
158 protected $return_url; |
|
159 |
|
160 /** |
|
161 * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused. |
|
162 * |
|
163 * @since 4.4.0 |
|
164 * @var array |
|
165 */ |
|
166 protected $autofocus = array(); |
|
167 |
|
168 /** |
|
169 * Messenger channel. |
|
170 * |
|
171 * @since 4.7.0 |
|
172 * @var string |
|
173 */ |
|
174 protected $messenger_channel; |
|
175 |
|
176 /** |
|
177 * Whether the autosave revision of the changeset should be loaded. |
|
178 * |
|
179 * @since 4.9.0 |
|
180 * @var bool |
|
181 */ |
|
182 protected $autosaved = false; |
|
183 |
|
184 /** |
|
185 * Whether the changeset branching is allowed. |
|
186 * |
|
187 * @since 4.9.0 |
|
188 * @var bool |
|
189 */ |
|
190 protected $branching = true; |
|
191 |
|
192 /** |
|
193 * Whether settings should be previewed. |
|
194 * |
|
195 * @since 4.9.0 |
|
196 * @var bool |
|
197 */ |
|
198 protected $settings_previewed = true; |
|
199 |
|
200 /** |
|
201 * Whether a starter content changeset was saved. |
|
202 * |
|
203 * @since 4.9.0 |
|
204 * @var bool |
|
205 */ |
|
206 protected $saved_starter_content_changeset = false; |
|
207 |
|
208 /** |
72 * Unsanitized values for Customize Settings parsed from $_POST['customized']. |
209 * Unsanitized values for Customize Settings parsed from $_POST['customized']. |
73 * |
210 * |
74 * @var array |
211 * @var array |
75 */ |
212 */ |
76 private $_post_values; |
213 private $_post_values; |
77 |
214 |
78 /** |
215 /** |
|
216 * Changeset UUID. |
|
217 * |
|
218 * @since 4.7.0 |
|
219 * @var string |
|
220 */ |
|
221 private $_changeset_uuid; |
|
222 |
|
223 /** |
|
224 * Changeset post ID. |
|
225 * |
|
226 * @since 4.7.0 |
|
227 * @var int|false |
|
228 */ |
|
229 private $_changeset_post_id; |
|
230 |
|
231 /** |
|
232 * Changeset data loaded from a customize_changeset post. |
|
233 * |
|
234 * @since 4.7.0 |
|
235 * @var array |
|
236 */ |
|
237 private $_changeset_data; |
|
238 |
|
239 /** |
79 * Constructor. |
240 * Constructor. |
80 * |
241 * |
81 * @since 3.4.0 |
242 * @since 3.4.0 |
82 */ |
243 * @since 4.7.0 Added $args param. |
83 public function __construct() { |
244 * |
|
245 * @param array $args { |
|
246 * Args. |
|
247 * |
|
248 * @type null|string|false $changeset_uuid Changeset UUID, the `post_name` for the customize_changeset post containing the customized state. |
|
249 * Defaults to `null` resulting in a UUID to be immediately generated. If `false` is provided, then |
|
250 * then the changeset UUID will be determined during `after_setup_theme`: when the |
|
251 * `customize_changeset_branching` filter returns false, then the default UUID will be that |
|
252 * of the most recent `customize_changeset` post that has a status other than 'auto-draft', |
|
253 * 'publish', or 'trash'. Otherwise, if changeset branching is enabled, then a random UUID will be used. |
|
254 * @type string $theme Theme to be previewed (for theme switch). Defaults to customize_theme or theme query params. |
|
255 * @type string $messenger_channel Messenger channel. Defaults to customize_messenger_channel query param. |
|
256 * @type bool $settings_previewed If settings should be previewed. Defaults to true. |
|
257 * @type bool $branching If changeset branching is allowed; otherwise, changesets are linear. Defaults to true. |
|
258 * @type bool $autosaved If data from a changeset's autosaved revision should be loaded if it exists. Defaults to false. |
|
259 * } |
|
260 */ |
|
261 public function __construct( $args = array() ) { |
|
262 |
|
263 $args = array_merge( |
|
264 array_fill_keys( array( 'changeset_uuid', 'theme', 'messenger_channel', 'settings_previewed', 'autosaved', 'branching' ), null ), |
|
265 $args |
|
266 ); |
|
267 |
|
268 // Note that the UUID format will be validated in the setup_theme() method. |
|
269 if ( ! isset( $args['changeset_uuid'] ) ) { |
|
270 $args['changeset_uuid'] = wp_generate_uuid4(); |
|
271 } |
|
272 |
|
273 // The theme and messenger_channel should be supplied via $args, but they are also looked at in the $_REQUEST global here for back-compat. |
|
274 if ( ! isset( $args['theme'] ) ) { |
|
275 if ( isset( $_REQUEST['customize_theme'] ) ) { |
|
276 $args['theme'] = wp_unslash( $_REQUEST['customize_theme'] ); |
|
277 } elseif ( isset( $_REQUEST['theme'] ) ) { // Deprecated. |
|
278 $args['theme'] = wp_unslash( $_REQUEST['theme'] ); |
|
279 } |
|
280 } |
|
281 if ( ! isset( $args['messenger_channel'] ) && isset( $_REQUEST['customize_messenger_channel'] ) ) { |
|
282 $args['messenger_channel'] = sanitize_key( wp_unslash( $_REQUEST['customize_messenger_channel'] ) ); |
|
283 } |
|
284 |
|
285 $this->original_stylesheet = get_stylesheet(); |
|
286 $this->theme = wp_get_theme( 0 === validate_file( $args['theme'] ) ? $args['theme'] : null ); |
|
287 $this->messenger_channel = $args['messenger_channel']; |
|
288 $this->_changeset_uuid = $args['changeset_uuid']; |
|
289 |
|
290 foreach ( array( 'settings_previewed', 'autosaved', 'branching' ) as $key ) { |
|
291 if ( isset( $args[ $key ] ) ) { |
|
292 $this->$key = (bool) $args[ $key ]; |
|
293 } |
|
294 } |
|
295 |
84 require_once( ABSPATH . WPINC . '/class-wp-customize-setting.php' ); |
296 require_once( ABSPATH . WPINC . '/class-wp-customize-setting.php' ); |
85 require_once( ABSPATH . WPINC . '/class-wp-customize-panel.php' ); |
297 require_once( ABSPATH . WPINC . '/class-wp-customize-panel.php' ); |
86 require_once( ABSPATH . WPINC . '/class-wp-customize-section.php' ); |
298 require_once( ABSPATH . WPINC . '/class-wp-customize-section.php' ); |
87 require_once( ABSPATH . WPINC . '/class-wp-customize-control.php' ); |
299 require_once( ABSPATH . WPINC . '/class-wp-customize-control.php' ); |
88 require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' ); |
300 |
89 |
301 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-color-control.php' ); |
90 $this->widgets = new WP_Customize_Widgets( $this ); |
302 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-media-control.php' ); |
91 |
303 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-upload-control.php' ); |
92 add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) ); |
304 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-image-control.php' ); |
|
305 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-control.php' ); |
|
306 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-position-control.php' ); |
|
307 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-cropped-image-control.php' ); |
|
308 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-site-icon-control.php' ); |
|
309 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-control.php' ); |
|
310 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-theme-control.php' ); |
|
311 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-code-editor-control.php' ); |
|
312 require_once( ABSPATH . WPINC . '/customize/class-wp-widget-area-customize-control.php' ); |
|
313 require_once( ABSPATH . WPINC . '/customize/class-wp-widget-form-customize-control.php' ); |
|
314 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-control.php' ); |
|
315 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-control.php' ); |
|
316 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-location-control.php' ); |
|
317 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-name-control.php' ); |
|
318 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-locations-control.php' ); |
|
319 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-auto-add-control.php' ); |
|
320 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-control.php' ); // @todo Remove in 5.0. See #42364. |
|
321 |
|
322 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php' ); |
|
323 |
|
324 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-panel.php' ); |
|
325 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php' ); |
|
326 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php' ); |
|
327 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-section.php' ); |
|
328 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-section.php' ); // @todo Remove in 5.0. See #42364. |
|
329 |
|
330 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-custom-css-setting.php' ); |
|
331 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-filter-setting.php' ); |
|
332 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-setting.php' ); |
|
333 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-setting.php' ); |
|
334 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-setting.php' ); |
|
335 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-setting.php' ); |
|
336 |
|
337 /** |
|
338 * Filters the core Customizer components to load. |
|
339 * |
|
340 * This allows Core components to be excluded from being instantiated by |
|
341 * filtering them out of the array. Note that this filter generally runs |
|
342 * during the {@see 'plugins_loaded'} action, so it cannot be added |
|
343 * in a theme. |
|
344 * |
|
345 * @since 4.4.0 |
|
346 * |
|
347 * @see WP_Customize_Manager::__construct() |
|
348 * |
|
349 * @param array $components List of core components to load. |
|
350 * @param WP_Customize_Manager $this WP_Customize_Manager instance. |
|
351 */ |
|
352 $components = apply_filters( 'customize_loaded_components', $this->components, $this ); |
|
353 |
|
354 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php' ); |
|
355 $this->selective_refresh = new WP_Customize_Selective_Refresh( $this ); |
|
356 |
|
357 if ( in_array( 'widgets', $components, true ) ) { |
|
358 require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' ); |
|
359 $this->widgets = new WP_Customize_Widgets( $this ); |
|
360 } |
|
361 |
|
362 if ( in_array( 'nav_menus', $components, true ) ) { |
|
363 require_once( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' ); |
|
364 $this->nav_menus = new WP_Customize_Nav_Menus( $this ); |
|
365 } |
93 |
366 |
94 add_action( 'setup_theme', array( $this, 'setup_theme' ) ); |
367 add_action( 'setup_theme', array( $this, 'setup_theme' ) ); |
95 add_action( 'wp_loaded', array( $this, 'wp_loaded' ) ); |
368 add_action( 'wp_loaded', array( $this, 'wp_loaded' ) ); |
96 |
|
97 // Run wp_redirect_status late to make sure we override the status last. |
|
98 add_action( 'wp_redirect_status', array( $this, 'wp_redirect_status' ), 1000 ); |
|
99 |
369 |
100 // Do not spawn cron (especially the alternate cron) while running the Customizer. |
370 // Do not spawn cron (especially the alternate cron) while running the Customizer. |
101 remove_action( 'init', 'wp_cron' ); |
371 remove_action( 'init', 'wp_cron' ); |
102 |
372 |
103 // Do not run update checks when rendering the controls. |
373 // Do not run update checks when rendering the controls. |
104 remove_action( 'admin_init', '_maybe_update_core' ); |
374 remove_action( 'admin_init', '_maybe_update_core' ); |
105 remove_action( 'admin_init', '_maybe_update_plugins' ); |
375 remove_action( 'admin_init', '_maybe_update_plugins' ); |
106 remove_action( 'admin_init', '_maybe_update_themes' ); |
376 remove_action( 'admin_init', '_maybe_update_themes' ); |
107 |
377 |
108 add_action( 'wp_ajax_customize_save', array( $this, 'save' ) ); |
378 add_action( 'wp_ajax_customize_save', array( $this, 'save' ) ); |
109 add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) ); |
379 add_action( 'wp_ajax_customize_trash', array( $this, 'handle_changeset_trash_request' ) ); |
|
380 add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) ); |
|
381 add_action( 'wp_ajax_customize_load_themes', array( $this, 'handle_load_themes_request' ) ); |
|
382 add_filter( 'heartbeat_settings', array( $this, 'add_customize_screen_to_heartbeat_settings' ) ); |
|
383 add_filter( 'heartbeat_received', array( $this, 'check_changeset_lock_with_heartbeat' ), 10, 3 ); |
|
384 add_action( 'wp_ajax_customize_override_changeset_lock', array( $this, 'handle_override_changeset_lock_request' ) ); |
|
385 add_action( 'wp_ajax_customize_dismiss_autosave_or_lock', array( $this, 'handle_dismiss_autosave_or_lock_request' ) ); |
110 |
386 |
111 add_action( 'customize_register', array( $this, 'register_controls' ) ); |
387 add_action( 'customize_register', array( $this, 'register_controls' ) ); |
112 add_action( 'customize_register', array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first |
388 add_action( 'customize_register', array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first |
113 add_action( 'customize_controls_init', array( $this, 'prepare_controls' ) ); |
389 add_action( 'customize_controls_init', array( $this, 'prepare_controls' ) ); |
114 add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) ); |
390 add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) ); |
115 } |
391 |
116 |
392 // Render Common, Panel, Section, and Control templates. |
117 /** |
393 add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_panel_templates' ), 1 ); |
118 * Return true if it's an AJAX request. |
394 add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_section_templates' ), 1 ); |
|
395 add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_control_templates' ), 1 ); |
|
396 |
|
397 // Export header video settings with the partial response. |
|
398 add_filter( 'customize_render_partials_response', array( $this, 'export_header_video_settings' ), 10, 3 ); |
|
399 |
|
400 // Export the settings to JS via the _wpCustomizeSettings variable. |
|
401 add_action( 'customize_controls_print_footer_scripts', array( $this, 'customize_pane_settings' ), 1000 ); |
|
402 |
|
403 // Add theme update notices. |
|
404 if ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) ) { |
|
405 require_once ABSPATH . '/wp-admin/includes/update.php'; |
|
406 add_action( 'customize_controls_print_footer_scripts', 'wp_print_admin_notice_templates' ); |
|
407 } |
|
408 } |
|
409 |
|
410 /** |
|
411 * Return true if it's an Ajax request. |
119 * |
412 * |
120 * @since 3.4.0 |
413 * @since 3.4.0 |
121 * @since 4.2.0 Added `$action` param. |
414 * @since 4.2.0 Added `$action` param. |
122 * @access public |
415 * |
123 * |
416 * @param string|null $action Whether the supplied Ajax action is being run. |
124 * @param string|null $action Whether the supplied AJAX action is being run. |
417 * @return bool True if it's an Ajax request, false otherwise. |
125 * @return bool True if it's an AJAX request, false otherwise. |
|
126 */ |
418 */ |
127 public function doing_ajax( $action = null ) { |
419 public function doing_ajax( $action = null ) { |
128 $doing_ajax = ( defined( 'DOING_AJAX' ) && DOING_AJAX ); |
420 if ( ! wp_doing_ajax() ) { |
129 if ( ! $doing_ajax ) { |
|
130 return false; |
421 return false; |
131 } |
422 } |
132 |
423 |
133 if ( ! $action ) { |
424 if ( ! $action ) { |
134 return true; |
425 return true; |
402 * Register styles/scripts and initialize the preview of each setting |
895 * Register styles/scripts and initialize the preview of each setting |
403 * |
896 * |
404 * @since 3.4.0 |
897 * @since 3.4.0 |
405 */ |
898 */ |
406 public function wp_loaded() { |
899 public function wp_loaded() { |
|
900 |
|
901 // Unconditionally register core types for panels, sections, and controls in case plugin unhooks all customize_register actions. |
|
902 $this->register_panel_type( 'WP_Customize_Panel' ); |
|
903 $this->register_panel_type( 'WP_Customize_Themes_Panel' ); |
|
904 $this->register_section_type( 'WP_Customize_Section' ); |
|
905 $this->register_section_type( 'WP_Customize_Sidebar_Section' ); |
|
906 $this->register_section_type( 'WP_Customize_Themes_Section' ); |
|
907 $this->register_control_type( 'WP_Customize_Color_Control' ); |
|
908 $this->register_control_type( 'WP_Customize_Media_Control' ); |
|
909 $this->register_control_type( 'WP_Customize_Upload_Control' ); |
|
910 $this->register_control_type( 'WP_Customize_Image_Control' ); |
|
911 $this->register_control_type( 'WP_Customize_Background_Image_Control' ); |
|
912 $this->register_control_type( 'WP_Customize_Background_Position_Control' ); |
|
913 $this->register_control_type( 'WP_Customize_Cropped_Image_Control' ); |
|
914 $this->register_control_type( 'WP_Customize_Site_Icon_Control' ); |
|
915 $this->register_control_type( 'WP_Customize_Theme_Control' ); |
|
916 $this->register_control_type( 'WP_Customize_Code_Editor_Control' ); |
|
917 $this->register_control_type( 'WP_Customize_Date_Time_Control' ); |
407 |
918 |
408 /** |
919 /** |
409 * Fires once WordPress has loaded, allowing scripts and styles to be initialized. |
920 * Fires once WordPress has loaded, allowing scripts and styles to be initialized. |
410 * |
921 * |
411 * @since 3.4.0 |
922 * @since 3.4.0 |
412 * |
923 * |
413 * @param WP_Customize_Manager $this WP_Customize_Manager instance. |
924 * @param WP_Customize_Manager $this WP_Customize_Manager instance. |
414 */ |
925 */ |
415 do_action( 'customize_register', $this ); |
926 do_action( 'customize_register', $this ); |
416 |
927 |
417 if ( $this->is_preview() && ! is_admin() ) |
928 if ( $this->settings_previewed() ) { |
|
929 foreach ( $this->settings as $setting ) { |
|
930 $setting->preview(); |
|
931 } |
|
932 } |
|
933 |
|
934 if ( $this->is_preview() && ! is_admin() ) { |
418 $this->customize_preview_init(); |
935 $this->customize_preview_init(); |
419 } |
936 } |
420 |
937 } |
421 /** |
938 |
422 * Prevents AJAX requests from following redirects when previewing a theme |
939 /** |
|
940 * Prevents Ajax requests from following redirects when previewing a theme |
423 * by issuing a 200 response instead of a 30x. |
941 * by issuing a 200 response instead of a 30x. |
424 * |
942 * |
425 * Instead, the JS will sniff out the location header. |
943 * Instead, the JS will sniff out the location header. |
426 * |
944 * |
427 * @since 3.4.0 |
945 * @since 3.4.0 |
428 * |
946 * @deprecated 4.7.0 |
429 * @param $status |
947 * |
|
948 * @param int $status Status. |
430 * @return int |
949 * @return int |
431 */ |
950 */ |
432 public function wp_redirect_status( $status ) { |
951 public function wp_redirect_status( $status ) { |
433 if ( $this->is_preview() && ! is_admin() ) |
952 _deprecated_function( __FUNCTION__, '4.7.0' ); |
|
953 |
|
954 if ( $this->is_preview() && ! is_admin() ) { |
434 return 200; |
955 return 200; |
|
956 } |
435 |
957 |
436 return $status; |
958 return $status; |
437 } |
959 } |
438 |
960 |
439 /** |
961 /** |
440 * Parse the incoming $_POST['customized'] JSON data and store the unsanitized |
962 * Find the changeset post ID for a given changeset UUID. |
441 * settings for subsequent post_value() lookups. |
963 * |
|
964 * @since 4.7.0 |
|
965 * |
|
966 * @param string $uuid Changeset UUID. |
|
967 * @return int|null Returns post ID on success and null on failure. |
|
968 */ |
|
969 public function find_changeset_post_id( $uuid ) { |
|
970 $cache_group = 'customize_changeset_post'; |
|
971 $changeset_post_id = wp_cache_get( $uuid, $cache_group ); |
|
972 if ( $changeset_post_id && 'customize_changeset' === get_post_type( $changeset_post_id ) ) { |
|
973 return $changeset_post_id; |
|
974 } |
|
975 |
|
976 $changeset_post_query = new WP_Query( array( |
|
977 'post_type' => 'customize_changeset', |
|
978 'post_status' => get_post_stati(), |
|
979 'name' => $uuid, |
|
980 'posts_per_page' => 1, |
|
981 'no_found_rows' => true, |
|
982 'cache_results' => true, |
|
983 'update_post_meta_cache' => false, |
|
984 'update_post_term_cache' => false, |
|
985 'lazy_load_term_meta' => false, |
|
986 ) ); |
|
987 if ( ! empty( $changeset_post_query->posts ) ) { |
|
988 // Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed. |
|
989 $changeset_post_id = $changeset_post_query->posts[0]->ID; |
|
990 wp_cache_set( $uuid, $changeset_post_id, $cache_group ); |
|
991 return $changeset_post_id; |
|
992 } |
|
993 |
|
994 return null; |
|
995 } |
|
996 |
|
997 /** |
|
998 * Get changeset posts. |
|
999 * |
|
1000 * @since 4.9.0 |
|
1001 * |
|
1002 * @param array $args { |
|
1003 * Args to pass into `get_posts()` to query changesets. |
|
1004 * |
|
1005 * @type int $posts_per_page Number of posts to return. Defaults to -1 (all posts). |
|
1006 * @type int $author Post author. Defaults to current user. |
|
1007 * @type string $post_status Status of changeset. Defaults to 'auto-draft'. |
|
1008 * @type bool $exclude_restore_dismissed Whether to exclude changeset auto-drafts that have been dismissed. Defaults to true. |
|
1009 * } |
|
1010 * @return WP_Post[] Auto-draft changesets. |
|
1011 */ |
|
1012 protected function get_changeset_posts( $args = array() ) { |
|
1013 $default_args = array( |
|
1014 'exclude_restore_dismissed' => true, |
|
1015 'posts_per_page' => -1, |
|
1016 'post_type' => 'customize_changeset', |
|
1017 'post_status' => 'auto-draft', |
|
1018 'order' => 'DESC', |
|
1019 'orderby' => 'date', |
|
1020 'no_found_rows' => true, |
|
1021 'cache_results' => true, |
|
1022 'update_post_meta_cache' => false, |
|
1023 'update_post_term_cache' => false, |
|
1024 'lazy_load_term_meta' => false, |
|
1025 ); |
|
1026 if ( get_current_user_id() ) { |
|
1027 $default_args['author'] = get_current_user_id(); |
|
1028 } |
|
1029 $args = array_merge( $default_args, $args ); |
|
1030 |
|
1031 if ( ! empty( $args['exclude_restore_dismissed'] ) ) { |
|
1032 unset( $args['exclude_restore_dismissed'] ); |
|
1033 $args['meta_query'] = array( |
|
1034 array( |
|
1035 'key' => '_customize_restore_dismissed', |
|
1036 'compare' => 'NOT EXISTS', |
|
1037 ), |
|
1038 ); |
|
1039 } |
|
1040 |
|
1041 return get_posts( $args ); |
|
1042 } |
|
1043 |
|
1044 /** |
|
1045 * Dismiss all of the current user's auto-drafts (other than the present one). |
|
1046 * |
|
1047 * @since 4.9.0 |
|
1048 * @return int The number of auto-drafts that were dismissed. |
|
1049 */ |
|
1050 protected function dismiss_user_auto_draft_changesets() { |
|
1051 $changeset_autodraft_posts = $this->get_changeset_posts( array( |
|
1052 'post_status' => 'auto-draft', |
|
1053 'exclude_restore_dismissed' => true, |
|
1054 'posts_per_page' => -1, |
|
1055 ) ); |
|
1056 $dismissed = 0; |
|
1057 foreach ( $changeset_autodraft_posts as $autosave_autodraft_post ) { |
|
1058 if ( $autosave_autodraft_post->ID === $this->changeset_post_id() ) { |
|
1059 continue; |
|
1060 } |
|
1061 if ( update_post_meta( $autosave_autodraft_post->ID, '_customize_restore_dismissed', true ) ) { |
|
1062 $dismissed++; |
|
1063 } |
|
1064 } |
|
1065 return $dismissed; |
|
1066 } |
|
1067 |
|
1068 /** |
|
1069 * Get the changeset post id for the loaded changeset. |
|
1070 * |
|
1071 * @since 4.7.0 |
|
1072 * |
|
1073 * @return int|null Post ID on success or null if there is no post yet saved. |
|
1074 */ |
|
1075 public function changeset_post_id() { |
|
1076 if ( ! isset( $this->_changeset_post_id ) ) { |
|
1077 $post_id = $this->find_changeset_post_id( $this->changeset_uuid() ); |
|
1078 if ( ! $post_id ) { |
|
1079 $post_id = false; |
|
1080 } |
|
1081 $this->_changeset_post_id = $post_id; |
|
1082 } |
|
1083 if ( false === $this->_changeset_post_id ) { |
|
1084 return null; |
|
1085 } |
|
1086 return $this->_changeset_post_id; |
|
1087 } |
|
1088 |
|
1089 /** |
|
1090 * Get the data stored in a changeset post. |
|
1091 * |
|
1092 * @since 4.7.0 |
|
1093 * |
|
1094 * @param int $post_id Changeset post ID. |
|
1095 * @return array|WP_Error Changeset data or WP_Error on error. |
|
1096 */ |
|
1097 protected function get_changeset_post_data( $post_id ) { |
|
1098 if ( ! $post_id ) { |
|
1099 return new WP_Error( 'empty_post_id' ); |
|
1100 } |
|
1101 $changeset_post = get_post( $post_id ); |
|
1102 if ( ! $changeset_post ) { |
|
1103 return new WP_Error( 'missing_post' ); |
|
1104 } |
|
1105 if ( 'revision' === $changeset_post->post_type ) { |
|
1106 if ( 'customize_changeset' !== get_post_type( $changeset_post->post_parent ) ) { |
|
1107 return new WP_Error( 'wrong_post_type' ); |
|
1108 } |
|
1109 } elseif ( 'customize_changeset' !== $changeset_post->post_type ) { |
|
1110 return new WP_Error( 'wrong_post_type' ); |
|
1111 } |
|
1112 $changeset_data = json_decode( $changeset_post->post_content, true ); |
|
1113 if ( function_exists( 'json_last_error' ) && json_last_error() ) { |
|
1114 return new WP_Error( 'json_parse_error', '', json_last_error() ); |
|
1115 } |
|
1116 if ( ! is_array( $changeset_data ) ) { |
|
1117 return new WP_Error( 'expected_array' ); |
|
1118 } |
|
1119 return $changeset_data; |
|
1120 } |
|
1121 |
|
1122 /** |
|
1123 * Get changeset data. |
|
1124 * |
|
1125 * @since 4.7.0 |
|
1126 * @since 4.9.0 This will return the changeset's data with a user's autosave revision merged on top, if one exists and $autosaved is true. |
|
1127 * |
|
1128 * @return array Changeset data. |
|
1129 */ |
|
1130 public function changeset_data() { |
|
1131 if ( isset( $this->_changeset_data ) ) { |
|
1132 return $this->_changeset_data; |
|
1133 } |
|
1134 $changeset_post_id = $this->changeset_post_id(); |
|
1135 if ( ! $changeset_post_id ) { |
|
1136 $this->_changeset_data = array(); |
|
1137 } else { |
|
1138 if ( $this->autosaved() && is_user_logged_in() ) { |
|
1139 $autosave_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() ); |
|
1140 if ( $autosave_post ) { |
|
1141 $data = $this->get_changeset_post_data( $autosave_post->ID ); |
|
1142 if ( ! is_wp_error( $data ) ) { |
|
1143 $this->_changeset_data = $data; |
|
1144 } |
|
1145 } |
|
1146 } |
|
1147 |
|
1148 // Load data from the changeset if it was not loaded from an autosave. |
|
1149 if ( ! isset( $this->_changeset_data ) ) { |
|
1150 $data = $this->get_changeset_post_data( $changeset_post_id ); |
|
1151 if ( ! is_wp_error( $data ) ) { |
|
1152 $this->_changeset_data = $data; |
|
1153 } else { |
|
1154 $this->_changeset_data = array(); |
|
1155 } |
|
1156 } |
|
1157 } |
|
1158 return $this->_changeset_data; |
|
1159 } |
|
1160 |
|
1161 /** |
|
1162 * Starter content setting IDs. |
|
1163 * |
|
1164 * @since 4.7.0 |
|
1165 * @var array |
|
1166 */ |
|
1167 protected $pending_starter_content_settings_ids = array(); |
|
1168 |
|
1169 /** |
|
1170 * Import theme starter content into the customized state. |
|
1171 * |
|
1172 * @since 4.7.0 |
|
1173 * |
|
1174 * @param array $starter_content Starter content. Defaults to `get_theme_starter_content()`. |
|
1175 */ |
|
1176 function import_theme_starter_content( $starter_content = array() ) { |
|
1177 if ( empty( $starter_content ) ) { |
|
1178 $starter_content = get_theme_starter_content(); |
|
1179 } |
|
1180 |
|
1181 $changeset_data = array(); |
|
1182 if ( $this->changeset_post_id() ) { |
|
1183 /* |
|
1184 * Don't re-import starter content into a changeset saved persistently. |
|
1185 * This will need to be revisited in the future once theme switching |
|
1186 * is allowed with drafted/scheduled changesets, since switching to |
|
1187 * another theme could result in more starter content being applied. |
|
1188 * However, when doing an explicit save it is currently possible for |
|
1189 * nav menus and nav menu items specifically to lose their starter_content |
|
1190 * flags, thus resulting in duplicates being created since they fail |
|
1191 * to get re-used. See #40146. |
|
1192 */ |
|
1193 if ( 'auto-draft' !== get_post_status( $this->changeset_post_id() ) ) { |
|
1194 return; |
|
1195 } |
|
1196 |
|
1197 $changeset_data = $this->get_changeset_post_data( $this->changeset_post_id() ); |
|
1198 } |
|
1199 |
|
1200 $sidebars_widgets = isset( $starter_content['widgets'] ) && ! empty( $this->widgets ) ? $starter_content['widgets'] : array(); |
|
1201 $attachments = isset( $starter_content['attachments'] ) && ! empty( $this->nav_menus ) ? $starter_content['attachments'] : array(); |
|
1202 $posts = isset( $starter_content['posts'] ) && ! empty( $this->nav_menus ) ? $starter_content['posts'] : array(); |
|
1203 $options = isset( $starter_content['options'] ) ? $starter_content['options'] : array(); |
|
1204 $nav_menus = isset( $starter_content['nav_menus'] ) && ! empty( $this->nav_menus ) ? $starter_content['nav_menus'] : array(); |
|
1205 $theme_mods = isset( $starter_content['theme_mods'] ) ? $starter_content['theme_mods'] : array(); |
|
1206 |
|
1207 // Widgets. |
|
1208 $max_widget_numbers = array(); |
|
1209 foreach ( $sidebars_widgets as $sidebar_id => $widgets ) { |
|
1210 $sidebar_widget_ids = array(); |
|
1211 foreach ( $widgets as $widget ) { |
|
1212 list( $id_base, $instance ) = $widget; |
|
1213 |
|
1214 if ( ! isset( $max_widget_numbers[ $id_base ] ) ) { |
|
1215 |
|
1216 // When $settings is an array-like object, get an intrinsic array for use with array_keys(). |
|
1217 $settings = get_option( "widget_{$id_base}", array() ); |
|
1218 if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) { |
|
1219 $settings = $settings->getArrayCopy(); |
|
1220 } |
|
1221 |
|
1222 // Find the max widget number for this type. |
|
1223 $widget_numbers = array_keys( $settings ); |
|
1224 if ( count( $widget_numbers ) > 0 ) { |
|
1225 $widget_numbers[] = 1; |
|
1226 $max_widget_numbers[ $id_base ] = call_user_func_array( 'max', $widget_numbers ); |
|
1227 } else { |
|
1228 $max_widget_numbers[ $id_base ] = 1; |
|
1229 } |
|
1230 } |
|
1231 $max_widget_numbers[ $id_base ] += 1; |
|
1232 |
|
1233 $widget_id = sprintf( '%s-%d', $id_base, $max_widget_numbers[ $id_base ] ); |
|
1234 $setting_id = sprintf( 'widget_%s[%d]', $id_base, $max_widget_numbers[ $id_base ] ); |
|
1235 |
|
1236 $setting_value = $this->widgets->sanitize_widget_js_instance( $instance ); |
|
1237 if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) { |
|
1238 $this->set_post_value( $setting_id, $setting_value ); |
|
1239 $this->pending_starter_content_settings_ids[] = $setting_id; |
|
1240 } |
|
1241 $sidebar_widget_ids[] = $widget_id; |
|
1242 } |
|
1243 |
|
1244 $setting_id = sprintf( 'sidebars_widgets[%s]', $sidebar_id ); |
|
1245 if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) { |
|
1246 $this->set_post_value( $setting_id, $sidebar_widget_ids ); |
|
1247 $this->pending_starter_content_settings_ids[] = $setting_id; |
|
1248 } |
|
1249 } |
|
1250 |
|
1251 $starter_content_auto_draft_post_ids = array(); |
|
1252 if ( ! empty( $changeset_data['nav_menus_created_posts']['value'] ) ) { |
|
1253 $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, $changeset_data['nav_menus_created_posts']['value'] ); |
|
1254 } |
|
1255 |
|
1256 // Make an index of all the posts needed and what their slugs are. |
|
1257 $needed_posts = array(); |
|
1258 $attachments = $this->prepare_starter_content_attachments( $attachments ); |
|
1259 foreach ( $attachments as $attachment ) { |
|
1260 $key = 'attachment:' . $attachment['post_name']; |
|
1261 $needed_posts[ $key ] = true; |
|
1262 } |
|
1263 foreach ( array_keys( $posts ) as $post_symbol ) { |
|
1264 if ( empty( $posts[ $post_symbol ]['post_name'] ) && empty( $posts[ $post_symbol ]['post_title'] ) ) { |
|
1265 unset( $posts[ $post_symbol ] ); |
|
1266 continue; |
|
1267 } |
|
1268 if ( empty( $posts[ $post_symbol ]['post_name'] ) ) { |
|
1269 $posts[ $post_symbol ]['post_name'] = sanitize_title( $posts[ $post_symbol ]['post_title'] ); |
|
1270 } |
|
1271 if ( empty( $posts[ $post_symbol ]['post_type'] ) ) { |
|
1272 $posts[ $post_symbol ]['post_type'] = 'post'; |
|
1273 } |
|
1274 $needed_posts[ $posts[ $post_symbol ]['post_type'] . ':' . $posts[ $post_symbol ]['post_name'] ] = true; |
|
1275 } |
|
1276 $all_post_slugs = array_merge( |
|
1277 wp_list_pluck( $attachments, 'post_name' ), |
|
1278 wp_list_pluck( $posts, 'post_name' ) |
|
1279 ); |
|
1280 |
|
1281 /* |
|
1282 * Obtain all post types referenced in starter content to use in query. |
|
1283 * This is needed because 'any' will not account for post types not yet registered. |
|
1284 */ |
|
1285 $post_types = array_filter( array_merge( array( 'attachment' ), wp_list_pluck( $posts, 'post_type' ) ) ); |
|
1286 |
|
1287 // Re-use auto-draft starter content posts referenced in the current customized state. |
|
1288 $existing_starter_content_posts = array(); |
|
1289 if ( ! empty( $starter_content_auto_draft_post_ids ) ) { |
|
1290 $existing_posts_query = new WP_Query( array( |
|
1291 'post__in' => $starter_content_auto_draft_post_ids, |
|
1292 'post_status' => 'auto-draft', |
|
1293 'post_type' => $post_types, |
|
1294 'posts_per_page' => -1, |
|
1295 ) ); |
|
1296 foreach ( $existing_posts_query->posts as $existing_post ) { |
|
1297 $post_name = $existing_post->post_name; |
|
1298 if ( empty( $post_name ) ) { |
|
1299 $post_name = get_post_meta( $existing_post->ID, '_customize_draft_post_name', true ); |
|
1300 } |
|
1301 $existing_starter_content_posts[ $existing_post->post_type . ':' . $post_name ] = $existing_post; |
|
1302 } |
|
1303 } |
|
1304 |
|
1305 // Re-use non-auto-draft posts. |
|
1306 if ( ! empty( $all_post_slugs ) ) { |
|
1307 $existing_posts_query = new WP_Query( array( |
|
1308 'post_name__in' => $all_post_slugs, |
|
1309 'post_status' => array_diff( get_post_stati(), array( 'auto-draft' ) ), |
|
1310 'post_type' => 'any', |
|
1311 'posts_per_page' => -1, |
|
1312 ) ); |
|
1313 foreach ( $existing_posts_query->posts as $existing_post ) { |
|
1314 $key = $existing_post->post_type . ':' . $existing_post->post_name; |
|
1315 if ( isset( $needed_posts[ $key ] ) && ! isset( $existing_starter_content_posts[ $key ] ) ) { |
|
1316 $existing_starter_content_posts[ $key ] = $existing_post; |
|
1317 } |
|
1318 } |
|
1319 } |
|
1320 |
|
1321 // Attachments are technically posts but handled differently. |
|
1322 if ( ! empty( $attachments ) ) { |
|
1323 |
|
1324 $attachment_ids = array(); |
|
1325 |
|
1326 foreach ( $attachments as $symbol => $attachment ) { |
|
1327 $file_array = array( |
|
1328 'name' => $attachment['file_name'], |
|
1329 ); |
|
1330 $file_path = $attachment['file_path']; |
|
1331 $attachment_id = null; |
|
1332 $attached_file = null; |
|
1333 if ( isset( $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ] ) ) { |
|
1334 $attachment_post = $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ]; |
|
1335 $attachment_id = $attachment_post->ID; |
|
1336 $attached_file = get_attached_file( $attachment_id ); |
|
1337 if ( empty( $attached_file ) || ! file_exists( $attached_file ) ) { |
|
1338 $attachment_id = null; |
|
1339 $attached_file = null; |
|
1340 } elseif ( $this->get_stylesheet() !== get_post_meta( $attachment_post->ID, '_starter_content_theme', true ) ) { |
|
1341 |
|
1342 // Re-generate attachment metadata since it was previously generated for a different theme. |
|
1343 $metadata = wp_generate_attachment_metadata( $attachment_post->ID, $attached_file ); |
|
1344 wp_update_attachment_metadata( $attachment_id, $metadata ); |
|
1345 update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() ); |
|
1346 } |
|
1347 } |
|
1348 |
|
1349 // Insert the attachment auto-draft because it doesn't yet exist or the attached file is gone. |
|
1350 if ( ! $attachment_id ) { |
|
1351 |
|
1352 // Copy file to temp location so that original file won't get deleted from theme after sideloading. |
|
1353 $temp_file_name = wp_tempnam( basename( $file_path ) ); |
|
1354 if ( $temp_file_name && copy( $file_path, $temp_file_name ) ) { |
|
1355 $file_array['tmp_name'] = $temp_file_name; |
|
1356 } |
|
1357 if ( empty( $file_array['tmp_name'] ) ) { |
|
1358 continue; |
|
1359 } |
|
1360 |
|
1361 $attachment_post_data = array_merge( |
|
1362 wp_array_slice_assoc( $attachment, array( 'post_title', 'post_content', 'post_excerpt' ) ), |
|
1363 array( |
|
1364 'post_status' => 'auto-draft', // So attachment will be garbage collected in a week if changeset is never published. |
|
1365 ) |
|
1366 ); |
|
1367 |
|
1368 // In PHP < 5.6 filesize() returns 0 for the temp files unless we clear the file status cache. |
|
1369 // Technically, PHP < 5.6.0 || < 5.5.13 || < 5.4.29 but no need to be so targeted. |
|
1370 // See https://bugs.php.net/bug.php?id=65701 |
|
1371 if ( version_compare( PHP_VERSION, '5.6', '<' ) ) { |
|
1372 clearstatcache(); |
|
1373 } |
|
1374 |
|
1375 $attachment_id = media_handle_sideload( $file_array, 0, null, $attachment_post_data ); |
|
1376 if ( is_wp_error( $attachment_id ) ) { |
|
1377 continue; |
|
1378 } |
|
1379 update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() ); |
|
1380 update_post_meta( $attachment_id, '_customize_draft_post_name', $attachment['post_name'] ); |
|
1381 } |
|
1382 |
|
1383 $attachment_ids[ $symbol ] = $attachment_id; |
|
1384 } |
|
1385 $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, array_values( $attachment_ids ) ); |
|
1386 } |
|
1387 |
|
1388 // Posts & pages. |
|
1389 if ( ! empty( $posts ) ) { |
|
1390 foreach ( array_keys( $posts ) as $post_symbol ) { |
|
1391 if ( empty( $posts[ $post_symbol ]['post_type'] ) || empty( $posts[ $post_symbol ]['post_name'] ) ) { |
|
1392 continue; |
|
1393 } |
|
1394 $post_type = $posts[ $post_symbol ]['post_type']; |
|
1395 if ( ! empty( $posts[ $post_symbol ]['post_name'] ) ) { |
|
1396 $post_name = $posts[ $post_symbol ]['post_name']; |
|
1397 } elseif ( ! empty( $posts[ $post_symbol ]['post_title'] ) ) { |
|
1398 $post_name = sanitize_title( $posts[ $post_symbol ]['post_title'] ); |
|
1399 } else { |
|
1400 continue; |
|
1401 } |
|
1402 |
|
1403 // Use existing auto-draft post if one already exists with the same type and name. |
|
1404 if ( isset( $existing_starter_content_posts[ $post_type . ':' . $post_name ] ) ) { |
|
1405 $posts[ $post_symbol ]['ID'] = $existing_starter_content_posts[ $post_type . ':' . $post_name ]->ID; |
|
1406 continue; |
|
1407 } |
|
1408 |
|
1409 // Translate the featured image symbol. |
|
1410 if ( ! empty( $posts[ $post_symbol ]['thumbnail'] ) |
|
1411 && preg_match( '/^{{(?P<symbol>.+)}}$/', $posts[ $post_symbol ]['thumbnail'], $matches ) |
|
1412 && isset( $attachment_ids[ $matches['symbol'] ] ) ) { |
|
1413 $posts[ $post_symbol ]['meta_input']['_thumbnail_id'] = $attachment_ids[ $matches['symbol'] ]; |
|
1414 } |
|
1415 |
|
1416 if ( ! empty( $posts[ $post_symbol ]['template'] ) ) { |
|
1417 $posts[ $post_symbol ]['meta_input']['_wp_page_template'] = $posts[ $post_symbol ]['template']; |
|
1418 } |
|
1419 |
|
1420 $r = $this->nav_menus->insert_auto_draft_post( $posts[ $post_symbol ] ); |
|
1421 if ( $r instanceof WP_Post ) { |
|
1422 $posts[ $post_symbol ]['ID'] = $r->ID; |
|
1423 } |
|
1424 } |
|
1425 |
|
1426 $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, wp_list_pluck( $posts, 'ID' ) ); |
|
1427 } |
|
1428 |
|
1429 // The nav_menus_created_posts setting is why nav_menus component is dependency for adding posts. |
|
1430 if ( ! empty( $this->nav_menus ) && ! empty( $starter_content_auto_draft_post_ids ) ) { |
|
1431 $setting_id = 'nav_menus_created_posts'; |
|
1432 $this->set_post_value( $setting_id, array_unique( array_values( $starter_content_auto_draft_post_ids ) ) ); |
|
1433 $this->pending_starter_content_settings_ids[] = $setting_id; |
|
1434 } |
|
1435 |
|
1436 // Nav menus. |
|
1437 $placeholder_id = -1; |
|
1438 $reused_nav_menu_setting_ids = array(); |
|
1439 foreach ( $nav_menus as $nav_menu_location => $nav_menu ) { |
|
1440 |
|
1441 $nav_menu_term_id = null; |
|
1442 $nav_menu_setting_id = null; |
|
1443 $matches = array(); |
|
1444 |
|
1445 // Look for an existing placeholder menu with starter content to re-use. |
|
1446 foreach ( $changeset_data as $setting_id => $setting_params ) { |
|
1447 $can_reuse = ( |
|
1448 ! empty( $setting_params['starter_content'] ) |
|
1449 && |
|
1450 ! in_array( $setting_id, $reused_nav_menu_setting_ids, true ) |
|
1451 && |
|
1452 preg_match( '#^nav_menu\[(?P<nav_menu_id>-?\d+)\]$#', $setting_id, $matches ) |
|
1453 ); |
|
1454 if ( $can_reuse ) { |
|
1455 $nav_menu_term_id = intval( $matches['nav_menu_id'] ); |
|
1456 $nav_menu_setting_id = $setting_id; |
|
1457 $reused_nav_menu_setting_ids[] = $setting_id; |
|
1458 break; |
|
1459 } |
|
1460 } |
|
1461 |
|
1462 if ( ! $nav_menu_term_id ) { |
|
1463 while ( isset( $changeset_data[ sprintf( 'nav_menu[%d]', $placeholder_id ) ] ) ) { |
|
1464 $placeholder_id--; |
|
1465 } |
|
1466 $nav_menu_term_id = $placeholder_id; |
|
1467 $nav_menu_setting_id = sprintf( 'nav_menu[%d]', $placeholder_id ); |
|
1468 } |
|
1469 |
|
1470 $this->set_post_value( $nav_menu_setting_id, array( |
|
1471 'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location, |
|
1472 ) ); |
|
1473 $this->pending_starter_content_settings_ids[] = $nav_menu_setting_id; |
|
1474 |
|
1475 // @todo Add support for menu_item_parent. |
|
1476 $position = 0; |
|
1477 foreach ( $nav_menu['items'] as $nav_menu_item ) { |
|
1478 $nav_menu_item_setting_id = sprintf( 'nav_menu_item[%d]', $placeholder_id-- ); |
|
1479 if ( ! isset( $nav_menu_item['position'] ) ) { |
|
1480 $nav_menu_item['position'] = $position++; |
|
1481 } |
|
1482 $nav_menu_item['nav_menu_term_id'] = $nav_menu_term_id; |
|
1483 |
|
1484 if ( isset( $nav_menu_item['object_id'] ) ) { |
|
1485 if ( 'post_type' === $nav_menu_item['type'] && preg_match( '/^{{(?P<symbol>.+)}}$/', $nav_menu_item['object_id'], $matches ) && isset( $posts[ $matches['symbol'] ] ) ) { |
|
1486 $nav_menu_item['object_id'] = $posts[ $matches['symbol'] ]['ID']; |
|
1487 if ( empty( $nav_menu_item['title'] ) ) { |
|
1488 $original_object = get_post( $nav_menu_item['object_id'] ); |
|
1489 $nav_menu_item['title'] = $original_object->post_title; |
|
1490 } |
|
1491 } else { |
|
1492 continue; |
|
1493 } |
|
1494 } else { |
|
1495 $nav_menu_item['object_id'] = 0; |
|
1496 } |
|
1497 |
|
1498 if ( empty( $changeset_data[ $nav_menu_item_setting_id ] ) || ! empty( $changeset_data[ $nav_menu_item_setting_id ]['starter_content'] ) ) { |
|
1499 $this->set_post_value( $nav_menu_item_setting_id, $nav_menu_item ); |
|
1500 $this->pending_starter_content_settings_ids[] = $nav_menu_item_setting_id; |
|
1501 } |
|
1502 } |
|
1503 |
|
1504 $setting_id = sprintf( 'nav_menu_locations[%s]', $nav_menu_location ); |
|
1505 if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) { |
|
1506 $this->set_post_value( $setting_id, $nav_menu_term_id ); |
|
1507 $this->pending_starter_content_settings_ids[] = $setting_id; |
|
1508 } |
|
1509 } |
|
1510 |
|
1511 // Options. |
|
1512 foreach ( $options as $name => $value ) { |
|
1513 if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) { |
|
1514 if ( isset( $posts[ $matches['symbol'] ] ) ) { |
|
1515 $value = $posts[ $matches['symbol'] ]['ID']; |
|
1516 } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) { |
|
1517 $value = $attachment_ids[ $matches['symbol'] ]; |
|
1518 } else { |
|
1519 continue; |
|
1520 } |
|
1521 } |
|
1522 |
|
1523 if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) { |
|
1524 $this->set_post_value( $name, $value ); |
|
1525 $this->pending_starter_content_settings_ids[] = $name; |
|
1526 } |
|
1527 } |
|
1528 |
|
1529 // Theme mods. |
|
1530 foreach ( $theme_mods as $name => $value ) { |
|
1531 if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) { |
|
1532 if ( isset( $posts[ $matches['symbol'] ] ) ) { |
|
1533 $value = $posts[ $matches['symbol'] ]['ID']; |
|
1534 } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) { |
|
1535 $value = $attachment_ids[ $matches['symbol'] ]; |
|
1536 } else { |
|
1537 continue; |
|
1538 } |
|
1539 } |
|
1540 |
|
1541 // Handle header image as special case since setting has a legacy format. |
|
1542 if ( 'header_image' === $name ) { |
|
1543 $name = 'header_image_data'; |
|
1544 $metadata = wp_get_attachment_metadata( $value ); |
|
1545 if ( empty( $metadata ) ) { |
|
1546 continue; |
|
1547 } |
|
1548 $value = array( |
|
1549 'attachment_id' => $value, |
|
1550 'url' => wp_get_attachment_url( $value ), |
|
1551 'height' => $metadata['height'], |
|
1552 'width' => $metadata['width'], |
|
1553 ); |
|
1554 } elseif ( 'background_image' === $name ) { |
|
1555 $value = wp_get_attachment_url( $value ); |
|
1556 } |
|
1557 |
|
1558 if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) { |
|
1559 $this->set_post_value( $name, $value ); |
|
1560 $this->pending_starter_content_settings_ids[] = $name; |
|
1561 } |
|
1562 } |
|
1563 |
|
1564 if ( ! empty( $this->pending_starter_content_settings_ids ) ) { |
|
1565 if ( did_action( 'customize_register' ) ) { |
|
1566 $this->_save_starter_content_changeset(); |
|
1567 } else { |
|
1568 add_action( 'customize_register', array( $this, '_save_starter_content_changeset' ), 1000 ); |
|
1569 } |
|
1570 } |
|
1571 } |
|
1572 |
|
1573 /** |
|
1574 * Prepare starter content attachments. |
|
1575 * |
|
1576 * Ensure that the attachments are valid and that they have slugs and file name/path. |
|
1577 * |
|
1578 * @since 4.7.0 |
|
1579 * |
|
1580 * @param array $attachments Attachments. |
|
1581 * @return array Prepared attachments. |
|
1582 */ |
|
1583 protected function prepare_starter_content_attachments( $attachments ) { |
|
1584 $prepared_attachments = array(); |
|
1585 if ( empty( $attachments ) ) { |
|
1586 return $prepared_attachments; |
|
1587 } |
|
1588 |
|
1589 // Such is The WordPress Way. |
|
1590 require_once( ABSPATH . 'wp-admin/includes/file.php' ); |
|
1591 require_once( ABSPATH . 'wp-admin/includes/media.php' ); |
|
1592 require_once( ABSPATH . 'wp-admin/includes/image.php' ); |
|
1593 |
|
1594 foreach ( $attachments as $symbol => $attachment ) { |
|
1595 |
|
1596 // A file is required and URLs to files are not currently allowed. |
|
1597 if ( empty( $attachment['file'] ) || preg_match( '#^https?://$#', $attachment['file'] ) ) { |
|
1598 continue; |
|
1599 } |
|
1600 |
|
1601 $file_path = null; |
|
1602 if ( file_exists( $attachment['file'] ) ) { |
|
1603 $file_path = $attachment['file']; // Could be absolute path to file in plugin. |
|
1604 } elseif ( is_child_theme() && file_exists( get_stylesheet_directory() . '/' . $attachment['file'] ) ) { |
|
1605 $file_path = get_stylesheet_directory() . '/' . $attachment['file']; |
|
1606 } elseif ( file_exists( get_template_directory() . '/' . $attachment['file'] ) ) { |
|
1607 $file_path = get_template_directory() . '/' . $attachment['file']; |
|
1608 } else { |
|
1609 continue; |
|
1610 } |
|
1611 $file_name = basename( $attachment['file'] ); |
|
1612 |
|
1613 // Skip file types that are not recognized. |
|
1614 $checked_filetype = wp_check_filetype( $file_name ); |
|
1615 if ( empty( $checked_filetype['type'] ) ) { |
|
1616 continue; |
|
1617 } |
|
1618 |
|
1619 // Ensure post_name is set since not automatically derived from post_title for new auto-draft posts. |
|
1620 if ( empty( $attachment['post_name'] ) ) { |
|
1621 if ( ! empty( $attachment['post_title'] ) ) { |
|
1622 $attachment['post_name'] = sanitize_title( $attachment['post_title'] ); |
|
1623 } else { |
|
1624 $attachment['post_name'] = sanitize_title( preg_replace( '/\.\w+$/', '', $file_name ) ); |
|
1625 } |
|
1626 } |
|
1627 |
|
1628 $attachment['file_name'] = $file_name; |
|
1629 $attachment['file_path'] = $file_path; |
|
1630 $prepared_attachments[ $symbol ] = $attachment; |
|
1631 } |
|
1632 return $prepared_attachments; |
|
1633 } |
|
1634 |
|
1635 /** |
|
1636 * Save starter content changeset. |
|
1637 * |
|
1638 * @since 4.7.0 |
|
1639 */ |
|
1640 public function _save_starter_content_changeset() { |
|
1641 |
|
1642 if ( empty( $this->pending_starter_content_settings_ids ) ) { |
|
1643 return; |
|
1644 } |
|
1645 |
|
1646 $this->save_changeset_post( array( |
|
1647 'data' => array_fill_keys( $this->pending_starter_content_settings_ids, array( 'starter_content' => true ) ), |
|
1648 'starter_content' => true, |
|
1649 ) ); |
|
1650 $this->saved_starter_content_changeset = true; |
|
1651 |
|
1652 $this->pending_starter_content_settings_ids = array(); |
|
1653 } |
|
1654 |
|
1655 /** |
|
1656 * Get dirty pre-sanitized setting values in the current customized state. |
|
1657 * |
|
1658 * The returned array consists of a merge of three sources: |
|
1659 * 1. If the theme is not currently active, then the base array is any stashed |
|
1660 * theme mods that were modified previously but never published. |
|
1661 * 2. The values from the current changeset, if it exists. |
|
1662 * 3. If the user can customize, the values parsed from the incoming |
|
1663 * `$_POST['customized']` JSON data. |
|
1664 * 4. Any programmatically-set post values via `WP_Customize_Manager::set_post_value()`. |
|
1665 * |
|
1666 * The name "unsanitized_post_values" is a carry-over from when the customized |
|
1667 * state was exclusively sourced from `$_POST['customized']`. Nevertheless, |
|
1668 * the value returned will come from the current changeset post and from the |
|
1669 * incoming post data. |
442 * |
1670 * |
443 * @since 4.1.1 |
1671 * @since 4.1.1 |
444 * |
1672 * @since 4.7.0 Added $args param and merging with changeset values and stashed theme mods. |
|
1673 * |
|
1674 * @param array $args { |
|
1675 * Args. |
|
1676 * |
|
1677 * @type bool $exclude_changeset Whether the changeset values should also be excluded. Defaults to false. |
|
1678 * @type bool $exclude_post_data Whether the post input values should also be excluded. Defaults to false when lacking the customize capability. |
|
1679 * } |
445 * @return array |
1680 * @return array |
446 */ |
1681 */ |
447 public function unsanitized_post_values() { |
1682 public function unsanitized_post_values( $args = array() ) { |
448 if ( ! isset( $this->_post_values ) ) { |
1683 $args = array_merge( |
449 if ( isset( $_POST['customized'] ) ) { |
1684 array( |
450 $this->_post_values = json_decode( wp_unslash( $_POST['customized'] ), true ); |
1685 'exclude_changeset' => false, |
451 } |
1686 'exclude_post_data' => ! current_user_can( 'customize' ), |
452 if ( empty( $this->_post_values ) ) { // if not isset or if JSON error |
1687 ), |
453 $this->_post_values = array(); |
1688 $args |
454 } |
1689 ); |
455 } |
1690 |
456 if ( empty( $this->_post_values ) ) { |
1691 $values = array(); |
457 return array(); |
1692 |
458 } else { |
1693 // Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present. |
459 return $this->_post_values; |
1694 if ( ! $this->is_theme_active() ) { |
460 } |
1695 $stashed_theme_mods = get_option( 'customize_stashed_theme_mods' ); |
461 } |
1696 $stylesheet = $this->get_stylesheet(); |
462 |
1697 if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) { |
463 /** |
1698 $values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) ); |
464 * Return the sanitized value for a given setting from the request's POST data. |
1699 } |
|
1700 } |
|
1701 |
|
1702 if ( ! $args['exclude_changeset'] ) { |
|
1703 foreach ( $this->changeset_data() as $setting_id => $setting_params ) { |
|
1704 if ( ! array_key_exists( 'value', $setting_params ) ) { |
|
1705 continue; |
|
1706 } |
|
1707 if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) { |
|
1708 |
|
1709 // Ensure that theme mods values are only used if they were saved under the current theme. |
|
1710 $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/'; |
|
1711 if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) { |
|
1712 $values[ $matches['setting_id'] ] = $setting_params['value']; |
|
1713 } |
|
1714 } else { |
|
1715 $values[ $setting_id ] = $setting_params['value']; |
|
1716 } |
|
1717 } |
|
1718 } |
|
1719 |
|
1720 if ( ! $args['exclude_post_data'] ) { |
|
1721 if ( ! isset( $this->_post_values ) ) { |
|
1722 if ( isset( $_POST['customized'] ) ) { |
|
1723 $post_values = json_decode( wp_unslash( $_POST['customized'] ), true ); |
|
1724 } else { |
|
1725 $post_values = array(); |
|
1726 } |
|
1727 if ( is_array( $post_values ) ) { |
|
1728 $this->_post_values = $post_values; |
|
1729 } else { |
|
1730 $this->_post_values = array(); |
|
1731 } |
|
1732 } |
|
1733 $values = array_merge( $values, $this->_post_values ); |
|
1734 } |
|
1735 return $values; |
|
1736 } |
|
1737 |
|
1738 /** |
|
1739 * Returns the sanitized value for a given setting from the current customized state. |
|
1740 * |
|
1741 * The name "post_value" is a carry-over from when the customized state was exclusively |
|
1742 * sourced from `$_POST['customized']`. Nevertheless, the value returned will come |
|
1743 * from the current changeset post and from the incoming post data. |
465 * |
1744 * |
466 * @since 3.4.0 |
1745 * @since 3.4.0 |
467 * @since 4.1.1 Introduced 'default' parameter. |
1746 * @since 4.1.1 Introduced the `$default` parameter. |
468 * |
1747 * @since 4.6.0 `$default` is now returned early when the setting post value is invalid. |
469 * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object |
1748 * |
470 * @param mixed $default value returned $setting has no post value (added in 4.2.0). |
1749 * @see WP_REST_Server::dispatch() |
471 * @return string|mixed $post_value Sanitized value or the $default provided |
1750 * @see WP_REST_Request::sanitize_params() |
|
1751 * @see WP_REST_Request::has_valid_params() |
|
1752 * |
|
1753 * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object. |
|
1754 * @param mixed $default Value returned $setting has no post value (added in 4.2.0) |
|
1755 * or the post value is invalid (added in 4.6.0). |
|
1756 * @return string|mixed $post_value Sanitized value or the $default provided. |
472 */ |
1757 */ |
473 public function post_value( $setting, $default = null ) { |
1758 public function post_value( $setting, $default = null ) { |
474 $post_values = $this->unsanitized_post_values(); |
1759 $post_values = $this->unsanitized_post_values(); |
475 if ( array_key_exists( $setting->id, $post_values ) ) { |
1760 if ( ! array_key_exists( $setting->id, $post_values ) ) { |
476 return $setting->sanitize( $post_values[ $setting->id ] ); |
|
477 } else { |
|
478 return $default; |
1761 return $default; |
479 } |
1762 } |
480 } |
1763 $value = $post_values[ $setting->id ]; |
481 |
1764 $valid = $setting->validate( $value ); |
482 /** |
1765 if ( is_wp_error( $valid ) ) { |
483 * Override a setting's (unsanitized) value as found in any incoming $_POST['customized']. |
1766 return $default; |
|
1767 } |
|
1768 $value = $setting->sanitize( $value ); |
|
1769 if ( is_null( $value ) || is_wp_error( $value ) ) { |
|
1770 return $default; |
|
1771 } |
|
1772 return $value; |
|
1773 } |
|
1774 |
|
1775 /** |
|
1776 * Override a setting's value in the current customized state. |
|
1777 * |
|
1778 * The name "post_value" is a carry-over from when the customized state was |
|
1779 * exclusively sourced from `$_POST['customized']`. |
484 * |
1780 * |
485 * @since 4.2.0 |
1781 * @since 4.2.0 |
486 * @access public |
|
487 * |
1782 * |
488 * @param string $setting_id ID for the WP_Customize_Setting instance. |
1783 * @param string $setting_id ID for the WP_Customize_Setting instance. |
489 * @param mixed $value Post value. |
1784 * @param mixed $value Post value. |
490 */ |
1785 */ |
491 public function set_post_value( $setting_id, $value ) { |
1786 public function set_post_value( $setting_id, $value ) { |
492 $this->unsanitized_post_values(); |
1787 $this->unsanitized_post_values(); // Populate _post_values from $_POST['customized']. |
493 $this->_post_values[ $setting_id ] = $value; |
1788 $this->_post_values[ $setting_id ] = $value; |
|
1789 |
|
1790 /** |
|
1791 * Announce when a specific setting's unsanitized post value has been set. |
|
1792 * |
|
1793 * Fires when the WP_Customize_Manager::set_post_value() method is called. |
|
1794 * |
|
1795 * The dynamic portion of the hook name, `$setting_id`, refers to the setting ID. |
|
1796 * |
|
1797 * @since 4.4.0 |
|
1798 * |
|
1799 * @param mixed $value Unsanitized setting post value. |
|
1800 * @param WP_Customize_Manager $this WP_Customize_Manager instance. |
|
1801 */ |
|
1802 do_action( "customize_post_value_set_{$setting_id}", $value, $this ); |
|
1803 |
|
1804 /** |
|
1805 * Announce when any setting's unsanitized post value has been set. |
|
1806 * |
|
1807 * Fires when the WP_Customize_Manager::set_post_value() method is called. |
|
1808 * |
|
1809 * This is useful for `WP_Customize_Setting` instances to watch |
|
1810 * in order to update a cached previewed value. |
|
1811 * |
|
1812 * @since 4.4.0 |
|
1813 * |
|
1814 * @param string $setting_id Setting ID. |
|
1815 * @param mixed $value Unsanitized setting post value. |
|
1816 * @param WP_Customize_Manager $this WP_Customize_Manager instance. |
|
1817 */ |
|
1818 do_action( 'customize_post_value_set', $setting_id, $value, $this ); |
494 } |
1819 } |
495 |
1820 |
496 /** |
1821 /** |
497 * Print JavaScript settings. |
1822 * Print JavaScript settings. |
498 * |
1823 * |
499 * @since 3.4.0 |
1824 * @since 3.4.0 |
500 */ |
1825 */ |
501 public function customize_preview_init() { |
1826 public function customize_preview_init() { |
502 $this->nonce_tick = check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'nonce' ); |
1827 |
|
1828 /* |
|
1829 * Now that Customizer previews are loaded into iframes via GET requests |
|
1830 * and natural URLs with transaction UUIDs added, we need to ensure that |
|
1831 * the responses are never cached by proxies. In practice, this will not |
|
1832 * be needed if the user is logged-in anyway. But if anonymous access is |
|
1833 * allowed then the auth cookies would not be sent and WordPress would |
|
1834 * not send no-cache headers by default. |
|
1835 */ |
|
1836 if ( ! headers_sent() ) { |
|
1837 nocache_headers(); |
|
1838 header( 'X-Robots: noindex, nofollow, noarchive' ); |
|
1839 } |
|
1840 add_action( 'wp_head', 'wp_no_robots' ); |
|
1841 add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) ); |
|
1842 |
|
1843 /* |
|
1844 * If preview is being served inside the customizer preview iframe, and |
|
1845 * if the user doesn't have customize capability, then it is assumed |
|
1846 * that the user's session has expired and they need to re-authenticate. |
|
1847 */ |
|
1848 if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) { |
|
1849 $this->wp_die( -1, __( 'Unauthorized. You may remove the customize_messenger_channel param to preview as frontend.' ) ); |
|
1850 return; |
|
1851 } |
503 |
1852 |
504 $this->prepare_controls(); |
1853 $this->prepare_controls(); |
505 |
1854 |
|
1855 add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) ); |
|
1856 |
506 wp_enqueue_script( 'customize-preview' ); |
1857 wp_enqueue_script( 'customize-preview' ); |
507 add_action( 'wp', array( $this, 'customize_preview_override_404_status' ) ); |
1858 wp_enqueue_style( 'customize-preview' ); |
508 add_action( 'wp_head', array( $this, 'customize_preview_base' ) ); |
|
509 add_action( 'wp_head', array( $this, 'customize_preview_html5' ) ); |
|
510 add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) ); |
1859 add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) ); |
|
1860 add_action( 'wp_head', array( $this, 'remove_frameless_preview_messenger_channel' ) ); |
511 add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 ); |
1861 add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 ); |
512 add_action( 'shutdown', array( $this, 'customize_preview_signature' ), 1000 ); |
1862 add_filter( 'get_edit_post_link', '__return_empty_string' ); |
513 add_filter( 'wp_die_handler', array( $this, 'remove_preview_signature' ) ); |
|
514 |
|
515 foreach ( $this->settings as $setting ) { |
|
516 $setting->preview(); |
|
517 } |
|
518 |
1863 |
519 /** |
1864 /** |
520 * Fires once the Customizer preview has initialized and JavaScript |
1865 * Fires once the Customizer preview has initialized and JavaScript |
521 * settings have been printed. |
1866 * settings have been printed. |
522 * |
1867 * |
720 public function current_theme( $current_theme ) { |
2235 public function current_theme( $current_theme ) { |
721 return $this->theme()->display('Name'); |
2236 return $this->theme()->display('Name'); |
722 } |
2237 } |
723 |
2238 |
724 /** |
2239 /** |
725 * Switch the theme and trigger the save() method on each setting. |
2240 * Validates setting values. |
|
2241 * |
|
2242 * Validation is skipped for unregistered settings or for values that are |
|
2243 * already null since they will be skipped anyway. Sanitization is applied |
|
2244 * to values that pass validation, and values that become null or `WP_Error` |
|
2245 * after sanitizing are marked invalid. |
|
2246 * |
|
2247 * @since 4.6.0 |
|
2248 * |
|
2249 * @see WP_REST_Request::has_valid_params() |
|
2250 * @see WP_Customize_Setting::validate() |
|
2251 * |
|
2252 * @param array $setting_values Mapping of setting IDs to values to validate and sanitize. |
|
2253 * @param array $options { |
|
2254 * Options. |
|
2255 * |
|
2256 * @type bool $validate_existence Whether a setting's existence will be checked. |
|
2257 * @type bool $validate_capability Whether the setting capability will be checked. |
|
2258 * } |
|
2259 * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`. |
|
2260 */ |
|
2261 public function validate_setting_values( $setting_values, $options = array() ) { |
|
2262 $options = wp_parse_args( $options, array( |
|
2263 'validate_capability' => false, |
|
2264 'validate_existence' => false, |
|
2265 ) ); |
|
2266 |
|
2267 $validities = array(); |
|
2268 foreach ( $setting_values as $setting_id => $unsanitized_value ) { |
|
2269 $setting = $this->get_setting( $setting_id ); |
|
2270 if ( ! $setting ) { |
|
2271 if ( $options['validate_existence'] ) { |
|
2272 $validities[ $setting_id ] = new WP_Error( 'unrecognized', __( 'Setting does not exist or is unrecognized.' ) ); |
|
2273 } |
|
2274 continue; |
|
2275 } |
|
2276 if ( $options['validate_capability'] && ! current_user_can( $setting->capability ) ) { |
|
2277 $validity = new WP_Error( 'unauthorized', __( 'Unauthorized to modify setting due to capability.' ) ); |
|
2278 } else { |
|
2279 if ( is_null( $unsanitized_value ) ) { |
|
2280 continue; |
|
2281 } |
|
2282 $validity = $setting->validate( $unsanitized_value ); |
|
2283 } |
|
2284 if ( ! is_wp_error( $validity ) ) { |
|
2285 /** This filter is documented in wp-includes/class-wp-customize-setting.php */ |
|
2286 $late_validity = apply_filters( "customize_validate_{$setting->id}", new WP_Error(), $unsanitized_value, $setting ); |
|
2287 if ( ! empty( $late_validity->errors ) ) { |
|
2288 $validity = $late_validity; |
|
2289 } |
|
2290 } |
|
2291 if ( ! is_wp_error( $validity ) ) { |
|
2292 $value = $setting->sanitize( $unsanitized_value ); |
|
2293 if ( is_null( $value ) ) { |
|
2294 $validity = false; |
|
2295 } elseif ( is_wp_error( $value ) ) { |
|
2296 $validity = $value; |
|
2297 } |
|
2298 } |
|
2299 if ( false === $validity ) { |
|
2300 $validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) ); |
|
2301 } |
|
2302 $validities[ $setting_id ] = $validity; |
|
2303 } |
|
2304 return $validities; |
|
2305 } |
|
2306 |
|
2307 /** |
|
2308 * Prepares setting validity for exporting to the client (JS). |
|
2309 * |
|
2310 * Converts `WP_Error` instance into array suitable for passing into the |
|
2311 * `wp.customize.Notification` JS model. |
|
2312 * |
|
2313 * @since 4.6.0 |
|
2314 * |
|
2315 * @param true|WP_Error $validity Setting validity. |
|
2316 * @return true|array If `$validity` was a WP_Error, the error codes will be array-mapped |
|
2317 * to their respective `message` and `data` to pass into the |
|
2318 * `wp.customize.Notification` JS model. |
|
2319 */ |
|
2320 public function prepare_setting_validity_for_js( $validity ) { |
|
2321 if ( is_wp_error( $validity ) ) { |
|
2322 $notification = array(); |
|
2323 foreach ( $validity->errors as $error_code => $error_messages ) { |
|
2324 $notification[ $error_code ] = array( |
|
2325 'message' => join( ' ', $error_messages ), |
|
2326 'data' => $validity->get_error_data( $error_code ), |
|
2327 ); |
|
2328 } |
|
2329 return $notification; |
|
2330 } else { |
|
2331 return true; |
|
2332 } |
|
2333 } |
|
2334 |
|
2335 /** |
|
2336 * Handle customize_save WP Ajax request to save/update a changeset. |
726 * |
2337 * |
727 * @since 3.4.0 |
2338 * @since 3.4.0 |
|
2339 * @since 4.7.0 The semantics of this method have changed to update a changeset, optionally to also change the status and other attributes. |
728 */ |
2340 */ |
729 public function save() { |
2341 public function save() { |
|
2342 if ( ! is_user_logged_in() ) { |
|
2343 wp_send_json_error( 'unauthenticated' ); |
|
2344 } |
|
2345 |
730 if ( ! $this->is_preview() ) { |
2346 if ( ! $this->is_preview() ) { |
731 wp_send_json_error( 'not_preview' ); |
2347 wp_send_json_error( 'not_preview' ); |
732 } |
2348 } |
733 |
2349 |
734 $action = 'save-customize_' . $this->get_stylesheet(); |
2350 $action = 'save-customize_' . $this->get_stylesheet(); |
735 if ( ! check_ajax_referer( $action, 'nonce', false ) ) { |
2351 if ( ! check_ajax_referer( $action, 'nonce', false ) ) { |
736 wp_send_json_error( 'invalid_nonce' ); |
2352 wp_send_json_error( 'invalid_nonce' ); |
737 } |
2353 } |
738 |
2354 |
739 // Do we have to switch themes? |
2355 $changeset_post_id = $this->changeset_post_id(); |
740 if ( ! $this->is_theme_active() ) { |
2356 $is_new_changeset = empty( $changeset_post_id ); |
741 // Temporarily stop previewing the theme to allow switch_themes() |
2357 if ( $is_new_changeset ) { |
742 // to operate properly. |
2358 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->create_posts ) ) { |
|
2359 wp_send_json_error( 'cannot_create_changeset_post' ); |
|
2360 } |
|
2361 } else { |
|
2362 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) { |
|
2363 wp_send_json_error( 'cannot_edit_changeset_post' ); |
|
2364 } |
|
2365 } |
|
2366 |
|
2367 if ( ! empty( $_POST['customize_changeset_data'] ) ) { |
|
2368 $input_changeset_data = json_decode( wp_unslash( $_POST['customize_changeset_data'] ), true ); |
|
2369 if ( ! is_array( $input_changeset_data ) ) { |
|
2370 wp_send_json_error( 'invalid_customize_changeset_data' ); |
|
2371 } |
|
2372 } else { |
|
2373 $input_changeset_data = array(); |
|
2374 } |
|
2375 |
|
2376 // Validate title. |
|
2377 $changeset_title = null; |
|
2378 if ( isset( $_POST['customize_changeset_title'] ) ) { |
|
2379 $changeset_title = sanitize_text_field( wp_unslash( $_POST['customize_changeset_title'] ) ); |
|
2380 } |
|
2381 |
|
2382 // Validate changeset status param. |
|
2383 $is_publish = null; |
|
2384 $changeset_status = null; |
|
2385 if ( isset( $_POST['customize_changeset_status'] ) ) { |
|
2386 $changeset_status = wp_unslash( $_POST['customize_changeset_status'] ); |
|
2387 if ( ! get_post_status_object( $changeset_status ) || ! in_array( $changeset_status, array( 'draft', 'pending', 'publish', 'future' ), true ) ) { |
|
2388 wp_send_json_error( 'bad_customize_changeset_status', 400 ); |
|
2389 } |
|
2390 $is_publish = ( 'publish' === $changeset_status || 'future' === $changeset_status ); |
|
2391 if ( $is_publish && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) { |
|
2392 wp_send_json_error( 'changeset_publish_unauthorized', 403 ); |
|
2393 } |
|
2394 } |
|
2395 |
|
2396 /* |
|
2397 * Validate changeset date param. Date is assumed to be in local time for |
|
2398 * the WP if in MySQL format (YYYY-MM-DD HH:MM:SS). Otherwise, the date |
|
2399 * is parsed with strtotime() so that ISO date format may be supplied |
|
2400 * or a string like "+10 minutes". |
|
2401 */ |
|
2402 $changeset_date_gmt = null; |
|
2403 if ( isset( $_POST['customize_changeset_date'] ) ) { |
|
2404 $changeset_date = wp_unslash( $_POST['customize_changeset_date'] ); |
|
2405 if ( preg_match( '/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/', $changeset_date ) ) { |
|
2406 $mm = substr( $changeset_date, 5, 2 ); |
|
2407 $jj = substr( $changeset_date, 8, 2 ); |
|
2408 $aa = substr( $changeset_date, 0, 4 ); |
|
2409 $valid_date = wp_checkdate( $mm, $jj, $aa, $changeset_date ); |
|
2410 if ( ! $valid_date ) { |
|
2411 wp_send_json_error( 'bad_customize_changeset_date', 400 ); |
|
2412 } |
|
2413 $changeset_date_gmt = get_gmt_from_date( $changeset_date ); |
|
2414 } else { |
|
2415 $timestamp = strtotime( $changeset_date ); |
|
2416 if ( ! $timestamp ) { |
|
2417 wp_send_json_error( 'bad_customize_changeset_date', 400 ); |
|
2418 } |
|
2419 $changeset_date_gmt = gmdate( 'Y-m-d H:i:s', $timestamp ); |
|
2420 } |
|
2421 } |
|
2422 |
|
2423 $lock_user_id = null; |
|
2424 $autosave = ! empty( $_POST['customize_changeset_autosave'] ); |
|
2425 if ( ! $is_new_changeset ) { |
|
2426 $lock_user_id = wp_check_post_lock( $this->changeset_post_id() ); |
|
2427 } |
|
2428 |
|
2429 // Force request to autosave when changeset is locked. |
|
2430 if ( $lock_user_id && ! $autosave ) { |
|
2431 $autosave = true; |
|
2432 $changeset_status = null; |
|
2433 $changeset_date_gmt = null; |
|
2434 } |
|
2435 |
|
2436 if ( $autosave && ! defined( 'DOING_AUTOSAVE' ) ) { // Back-compat. |
|
2437 define( 'DOING_AUTOSAVE', true ); |
|
2438 } |
|
2439 |
|
2440 $autosaved = false; |
|
2441 $r = $this->save_changeset_post( array( |
|
2442 'status' => $changeset_status, |
|
2443 'title' => $changeset_title, |
|
2444 'date_gmt' => $changeset_date_gmt, |
|
2445 'data' => $input_changeset_data, |
|
2446 'autosave' => $autosave, |
|
2447 ) ); |
|
2448 if ( $autosave && ! is_wp_error( $r ) ) { |
|
2449 $autosaved = true; |
|
2450 } |
|
2451 |
|
2452 // If the changeset was locked and an autosave request wasn't itself an error, then now explicitly return with a failure. |
|
2453 if ( $lock_user_id && ! is_wp_error( $r ) ) { |
|
2454 $r = new WP_Error( |
|
2455 'changeset_locked', |
|
2456 __( 'Changeset is being edited by other user.' ), |
|
2457 array( |
|
2458 'lock_user' => $this->get_lock_user_data( $lock_user_id ), |
|
2459 ) |
|
2460 ); |
|
2461 } |
|
2462 |
|
2463 if ( is_wp_error( $r ) ) { |
|
2464 $response = array( |
|
2465 'message' => $r->get_error_message(), |
|
2466 'code' => $r->get_error_code(), |
|
2467 ); |
|
2468 if ( is_array( $r->get_error_data() ) ) { |
|
2469 $response = array_merge( $response, $r->get_error_data() ); |
|
2470 } else { |
|
2471 $response['data'] = $r->get_error_data(); |
|
2472 } |
|
2473 } else { |
|
2474 $response = $r; |
|
2475 $changeset_post = get_post( $this->changeset_post_id() ); |
|
2476 |
|
2477 // Dismiss all other auto-draft changeset posts for this user (they serve like autosave revisions), as there should only be one. |
|
2478 if ( $is_new_changeset ) { |
|
2479 $this->dismiss_user_auto_draft_changesets(); |
|
2480 } |
|
2481 |
|
2482 // Note that if the changeset status was publish, then it will get set to trash if revisions are not supported. |
|
2483 $response['changeset_status'] = $changeset_post->post_status; |
|
2484 if ( $is_publish && 'trash' === $response['changeset_status'] ) { |
|
2485 $response['changeset_status'] = 'publish'; |
|
2486 } |
|
2487 |
|
2488 if ( 'publish' !== $response['changeset_status'] ) { |
|
2489 $this->set_changeset_lock( $changeset_post->ID ); |
|
2490 } |
|
2491 |
|
2492 if ( 'future' === $response['changeset_status'] ) { |
|
2493 $response['changeset_date'] = $changeset_post->post_date; |
|
2494 } |
|
2495 |
|
2496 if ( 'publish' === $response['changeset_status'] || 'trash' === $response['changeset_status'] ) { |
|
2497 $response['next_changeset_uuid'] = wp_generate_uuid4(); |
|
2498 } |
|
2499 } |
|
2500 |
|
2501 if ( $autosave ) { |
|
2502 $response['autosaved'] = $autosaved; |
|
2503 } |
|
2504 |
|
2505 if ( isset( $response['setting_validities'] ) ) { |
|
2506 $response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] ); |
|
2507 } |
|
2508 |
|
2509 /** |
|
2510 * Filters response data for a successful customize_save Ajax request. |
|
2511 * |
|
2512 * This filter does not apply if there was a nonce or authentication failure. |
|
2513 * |
|
2514 * @since 4.2.0 |
|
2515 * |
|
2516 * @param array $response Additional information passed back to the 'saved' |
|
2517 * event on `wp.customize`. |
|
2518 * @param WP_Customize_Manager $this WP_Customize_Manager instance. |
|
2519 */ |
|
2520 $response = apply_filters( 'customize_save_response', $response, $this ); |
|
2521 |
|
2522 if ( is_wp_error( $r ) ) { |
|
2523 wp_send_json_error( $response ); |
|
2524 } else { |
|
2525 wp_send_json_success( $response ); |
|
2526 } |
|
2527 } |
|
2528 |
|
2529 /** |
|
2530 * Save the post for the loaded changeset. |
|
2531 * |
|
2532 * @since 4.7.0 |
|
2533 * |
|
2534 * @param array $args { |
|
2535 * Args for changeset post. |
|
2536 * |
|
2537 * @type array $data Optional additional changeset data. Values will be merged on top of any existing post values. |
|
2538 * @type string $status Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed. |
|
2539 * @type string $title Post title. Optional. |
|
2540 * @type string $date_gmt Date in GMT. Optional. |
|
2541 * @type int $user_id ID for user who is saving the changeset. Optional, defaults to the current user ID. |
|
2542 * @type bool $starter_content Whether the data is starter content. If false (default), then $starter_content will be cleared for any $data being saved. |
|
2543 * @type bool $autosave Whether this is a request to create an autosave revision. |
|
2544 * } |
|
2545 * |
|
2546 * @return array|WP_Error Returns array on success and WP_Error with array data on error. |
|
2547 */ |
|
2548 function save_changeset_post( $args = array() ) { |
|
2549 |
|
2550 $args = array_merge( |
|
2551 array( |
|
2552 'status' => null, |
|
2553 'title' => null, |
|
2554 'data' => array(), |
|
2555 'date_gmt' => null, |
|
2556 'user_id' => get_current_user_id(), |
|
2557 'starter_content' => false, |
|
2558 'autosave' => false, |
|
2559 ), |
|
2560 $args |
|
2561 ); |
|
2562 |
|
2563 $changeset_post_id = $this->changeset_post_id(); |
|
2564 $existing_changeset_data = array(); |
|
2565 if ( $changeset_post_id ) { |
|
2566 $existing_status = get_post_status( $changeset_post_id ); |
|
2567 if ( 'publish' === $existing_status || 'trash' === $existing_status ) { |
|
2568 return new WP_Error( |
|
2569 'changeset_already_published', |
|
2570 __( 'The previous set of changes has already been published. Please try saving your current set of changes again.' ), |
|
2571 array( |
|
2572 'next_changeset_uuid' => wp_generate_uuid4(), |
|
2573 ) |
|
2574 ); |
|
2575 } |
|
2576 |
|
2577 $existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id ); |
|
2578 if ( is_wp_error( $existing_changeset_data ) ) { |
|
2579 return $existing_changeset_data; |
|
2580 } |
|
2581 } |
|
2582 |
|
2583 // Fail if attempting to publish but publish hook is missing. |
|
2584 if ( 'publish' === $args['status'] && false === has_action( 'transition_post_status', '_wp_customize_publish_changeset' ) ) { |
|
2585 return new WP_Error( 'missing_publish_callback' ); |
|
2586 } |
|
2587 |
|
2588 // Validate date. |
|
2589 $now = gmdate( 'Y-m-d H:i:59' ); |
|
2590 if ( $args['date_gmt'] ) { |
|
2591 $is_future_dated = ( mysql2date( 'U', $args['date_gmt'], false ) > mysql2date( 'U', $now, false ) ); |
|
2592 if ( ! $is_future_dated ) { |
|
2593 return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) ); // Only future dates are allowed. |
|
2594 } |
|
2595 |
|
2596 if ( ! $this->is_theme_active() && ( 'future' === $args['status'] || $is_future_dated ) ) { |
|
2597 return new WP_Error( 'cannot_schedule_theme_switches' ); // This should be allowed in the future, when theme is a regular setting. |
|
2598 } |
|
2599 $will_remain_auto_draft = ( ! $args['status'] && ( ! $changeset_post_id || 'auto-draft' === get_post_status( $changeset_post_id ) ) ); |
|
2600 if ( $will_remain_auto_draft ) { |
|
2601 return new WP_Error( 'cannot_supply_date_for_auto_draft_changeset' ); |
|
2602 } |
|
2603 } elseif ( $changeset_post_id && 'future' === $args['status'] ) { |
|
2604 |
|
2605 // Fail if the new status is future but the existing post's date is not in the future. |
|
2606 $changeset_post = get_post( $changeset_post_id ); |
|
2607 if ( mysql2date( 'U', $changeset_post->post_date_gmt, false ) <= mysql2date( 'U', $now, false ) ) { |
|
2608 return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) ); |
|
2609 } |
|
2610 } |
|
2611 |
|
2612 if ( ! empty( $is_future_dated ) && 'publish' === $args['status'] ) { |
|
2613 $args['status'] = 'future'; |
|
2614 } |
|
2615 |
|
2616 // Validate autosave param. See _wp_post_revision_fields() for why these fields are disallowed. |
|
2617 if ( $args['autosave'] ) { |
|
2618 if ( $args['date_gmt'] ) { |
|
2619 return new WP_Error( 'illegal_autosave_with_date_gmt' ); |
|
2620 } elseif ( $args['status'] ) { |
|
2621 return new WP_Error( 'illegal_autosave_with_status' ); |
|
2622 } elseif ( $args['user_id'] && get_current_user_id() !== $args['user_id'] ) { |
|
2623 return new WP_Error( 'illegal_autosave_with_non_current_user' ); |
|
2624 } |
|
2625 } |
|
2626 |
|
2627 // The request was made via wp.customize.previewer.save(). |
|
2628 $update_transactionally = (bool) $args['status']; |
|
2629 $allow_revision = (bool) $args['status']; |
|
2630 |
|
2631 // Amend post values with any supplied data. |
|
2632 foreach ( $args['data'] as $setting_id => $setting_params ) { |
|
2633 if ( is_array( $setting_params ) && array_key_exists( 'value', $setting_params ) ) { |
|
2634 $this->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized. |
|
2635 } |
|
2636 } |
|
2637 |
|
2638 // Note that in addition to post data, this will include any stashed theme mods. |
|
2639 $post_values = $this->unsanitized_post_values( array( |
|
2640 'exclude_changeset' => true, |
|
2641 'exclude_post_data' => false, |
|
2642 ) ); |
|
2643 $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value. |
|
2644 |
|
2645 /* |
|
2646 * Get list of IDs for settings that have values different from what is currently |
|
2647 * saved in the changeset. By skipping any values that are already the same, the |
|
2648 * subset of changed settings can be passed into validate_setting_values to prevent |
|
2649 * an underprivileged modifying a single setting for which they have the capability |
|
2650 * from being blocked from saving. This also prevents a user from touching of the |
|
2651 * previous saved settings and overriding the associated user_id if they made no change. |
|
2652 */ |
|
2653 $changed_setting_ids = array(); |
|
2654 foreach ( $post_values as $setting_id => $setting_value ) { |
|
2655 $setting = $this->get_setting( $setting_id ); |
|
2656 |
|
2657 if ( $setting && 'theme_mod' === $setting->type ) { |
|
2658 $prefixed_setting_id = $this->get_stylesheet() . '::' . $setting->id; |
|
2659 } else { |
|
2660 $prefixed_setting_id = $setting_id; |
|
2661 } |
|
2662 |
|
2663 $is_value_changed = ( |
|
2664 ! isset( $existing_changeset_data[ $prefixed_setting_id ] ) |
|
2665 || |
|
2666 ! array_key_exists( 'value', $existing_changeset_data[ $prefixed_setting_id ] ) |
|
2667 || |
|
2668 $existing_changeset_data[ $prefixed_setting_id ]['value'] !== $setting_value |
|
2669 ); |
|
2670 if ( $is_value_changed ) { |
|
2671 $changed_setting_ids[] = $setting_id; |
|
2672 } |
|
2673 } |
|
2674 |
|
2675 /** |
|
2676 * Fires before save validation happens. |
|
2677 * |
|
2678 * Plugins can add just-in-time {@see 'customize_validate_{$this->ID}'} filters |
|
2679 * at this point to catch any settings registered after `customize_register`. |
|
2680 * The dynamic portion of the hook name, `$this->ID` refers to the setting ID. |
|
2681 * |
|
2682 * @since 4.6.0 |
|
2683 * |
|
2684 * @param WP_Customize_Manager $this WP_Customize_Manager instance. |
|
2685 */ |
|
2686 do_action( 'customize_save_validation_before', $this ); |
|
2687 |
|
2688 // Validate settings. |
|
2689 $validated_values = array_merge( |
|
2690 array_fill_keys( array_keys( $args['data'] ), null ), // Make sure existence/capability checks are done on value-less setting updates. |
|
2691 $post_values |
|
2692 ); |
|
2693 $setting_validities = $this->validate_setting_values( $validated_values, array( |
|
2694 'validate_capability' => true, |
|
2695 'validate_existence' => true, |
|
2696 ) ); |
|
2697 $invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) ); |
|
2698 |
|
2699 /* |
|
2700 * Short-circuit if there are invalid settings the update is transactional. |
|
2701 * A changeset update is transactional when a status is supplied in the request. |
|
2702 */ |
|
2703 if ( $update_transactionally && $invalid_setting_count > 0 ) { |
|
2704 $response = array( |
|
2705 'setting_validities' => $setting_validities, |
|
2706 /* translators: %s: number of invalid settings */ |
|
2707 'message' => sprintf( _n( 'Unable to save due to %s invalid setting.', 'Unable to save due to %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ), |
|
2708 ); |
|
2709 return new WP_Error( 'transaction_fail', '', $response ); |
|
2710 } |
|
2711 |
|
2712 // Obtain/merge data for changeset. |
|
2713 $original_changeset_data = $this->get_changeset_post_data( $changeset_post_id ); |
|
2714 $data = $original_changeset_data; |
|
2715 if ( is_wp_error( $data ) ) { |
|
2716 $data = array(); |
|
2717 } |
|
2718 |
|
2719 // Ensure that all post values are included in the changeset data. |
|
2720 foreach ( $post_values as $setting_id => $post_value ) { |
|
2721 if ( ! isset( $args['data'][ $setting_id ] ) ) { |
|
2722 $args['data'][ $setting_id ] = array(); |
|
2723 } |
|
2724 if ( ! isset( $args['data'][ $setting_id ]['value'] ) ) { |
|
2725 $args['data'][ $setting_id ]['value'] = $post_value; |
|
2726 } |
|
2727 } |
|
2728 |
|
2729 foreach ( $args['data'] as $setting_id => $setting_params ) { |
|
2730 $setting = $this->get_setting( $setting_id ); |
|
2731 if ( ! $setting || ! $setting->check_capabilities() ) { |
|
2732 continue; |
|
2733 } |
|
2734 |
|
2735 // Skip updating changeset for invalid setting values. |
|
2736 if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) { |
|
2737 continue; |
|
2738 } |
|
2739 |
|
2740 $changeset_setting_id = $setting_id; |
|
2741 if ( 'theme_mod' === $setting->type ) { |
|
2742 $changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id ); |
|
2743 } |
|
2744 |
|
2745 if ( null === $setting_params ) { |
|
2746 // Remove setting from changeset entirely. |
|
2747 unset( $data[ $changeset_setting_id ] ); |
|
2748 } else { |
|
2749 |
|
2750 if ( ! isset( $data[ $changeset_setting_id ] ) ) { |
|
2751 $data[ $changeset_setting_id ] = array(); |
|
2752 } |
|
2753 |
|
2754 // Merge any additional setting params that have been supplied with the existing params. |
|
2755 $merged_setting_params = array_merge( $data[ $changeset_setting_id ], $setting_params ); |
|
2756 |
|
2757 // Skip updating setting params if unchanged (ensuring the user_id is not overwritten). |
|
2758 if ( $data[ $changeset_setting_id ] === $merged_setting_params ) { |
|
2759 continue; |
|
2760 } |
|
2761 |
|
2762 $data[ $changeset_setting_id ] = array_merge( |
|
2763 $merged_setting_params, |
|
2764 array( |
|
2765 'type' => $setting->type, |
|
2766 'user_id' => $args['user_id'], |
|
2767 'date_modified_gmt' => current_time( 'mysql', true ), |
|
2768 ) |
|
2769 ); |
|
2770 |
|
2771 // Clear starter_content flag in data if changeset is not explicitly being updated for starter content. |
|
2772 if ( empty( $args['starter_content'] ) ) { |
|
2773 unset( $data[ $changeset_setting_id ]['starter_content'] ); |
|
2774 } |
|
2775 } |
|
2776 } |
|
2777 |
|
2778 $filter_context = array( |
|
2779 'uuid' => $this->changeset_uuid(), |
|
2780 'title' => $args['title'], |
|
2781 'status' => $args['status'], |
|
2782 'date_gmt' => $args['date_gmt'], |
|
2783 'post_id' => $changeset_post_id, |
|
2784 'previous_data' => is_wp_error( $original_changeset_data ) ? array() : $original_changeset_data, |
|
2785 'manager' => $this, |
|
2786 ); |
|
2787 |
|
2788 /** |
|
2789 * Filters the settings' data that will be persisted into the changeset. |
|
2790 * |
|
2791 * Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter. |
|
2792 * |
|
2793 * @since 4.7.0 |
|
2794 * |
|
2795 * @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata. |
|
2796 * @param array $context { |
|
2797 * Filter context. |
|
2798 * |
|
2799 * @type string $uuid Changeset UUID. |
|
2800 * @type string $title Requested title for the changeset post. |
|
2801 * @type string $status Requested status for the changeset post. |
|
2802 * @type string $date_gmt Requested date for the changeset post in MySQL format and GMT timezone. |
|
2803 * @type int|false $post_id Post ID for the changeset, or false if it doesn't exist yet. |
|
2804 * @type array $previous_data Previous data contained in the changeset. |
|
2805 * @type WP_Customize_Manager $manager Manager instance. |
|
2806 * } |
|
2807 */ |
|
2808 $data = apply_filters( 'customize_changeset_save_data', $data, $filter_context ); |
|
2809 |
|
2810 // Switch theme if publishing changes now. |
|
2811 if ( 'publish' === $args['status'] && ! $this->is_theme_active() ) { |
|
2812 // Temporarily stop previewing the theme to allow switch_themes() to operate properly. |
743 $this->stop_previewing_theme(); |
2813 $this->stop_previewing_theme(); |
744 switch_theme( $this->get_stylesheet() ); |
2814 switch_theme( $this->get_stylesheet() ); |
745 update_option( 'theme_switched_via_customizer', true ); |
2815 update_option( 'theme_switched_via_customizer', true ); |
746 $this->start_previewing_theme(); |
2816 $this->start_previewing_theme(); |
747 } |
2817 } |
|
2818 |
|
2819 // Gather the data for wp_insert_post()/wp_update_post(). |
|
2820 $json_options = 0; |
|
2821 if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) { |
|
2822 $json_options |= JSON_UNESCAPED_SLASHES; // Introduced in PHP 5.4. This is only to improve readability as slashes needn't be escaped in storage. |
|
2823 } |
|
2824 $json_options |= JSON_PRETTY_PRINT; // Also introduced in PHP 5.4, but WP defines constant for back compat. See WP Trac #30139. |
|
2825 $post_array = array( |
|
2826 'post_content' => wp_json_encode( $data, $json_options ), |
|
2827 ); |
|
2828 if ( $args['title'] ) { |
|
2829 $post_array['post_title'] = $args['title']; |
|
2830 } |
|
2831 if ( $changeset_post_id ) { |
|
2832 $post_array['ID'] = $changeset_post_id; |
|
2833 } else { |
|
2834 $post_array['post_type'] = 'customize_changeset'; |
|
2835 $post_array['post_name'] = $this->changeset_uuid(); |
|
2836 $post_array['post_status'] = 'auto-draft'; |
|
2837 } |
|
2838 if ( $args['status'] ) { |
|
2839 $post_array['post_status'] = $args['status']; |
|
2840 } |
|
2841 |
|
2842 // Reset post date to now if we are publishing, otherwise pass post_date_gmt and translate for post_date. |
|
2843 if ( 'publish' === $args['status'] ) { |
|
2844 $post_array['post_date_gmt'] = '0000-00-00 00:00:00'; |
|
2845 $post_array['post_date'] = '0000-00-00 00:00:00'; |
|
2846 } elseif ( $args['date_gmt'] ) { |
|
2847 $post_array['post_date_gmt'] = $args['date_gmt']; |
|
2848 $post_array['post_date'] = get_date_from_gmt( $args['date_gmt'] ); |
|
2849 } elseif ( $changeset_post_id && 'auto-draft' === get_post_status( $changeset_post_id ) ) { |
|
2850 /* |
|
2851 * Keep bumping the date for the auto-draft whenever it is modified; |
|
2852 * this extends its life, preserving it from garbage-collection via |
|
2853 * wp_delete_auto_drafts(). |
|
2854 */ |
|
2855 $post_array['post_date'] = current_time( 'mysql' ); |
|
2856 $post_array['post_date_gmt'] = ''; |
|
2857 } |
|
2858 |
|
2859 $this->store_changeset_revision = $allow_revision; |
|
2860 add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 ); |
|
2861 |
|
2862 // Update the changeset post. The publish_customize_changeset action will cause the settings in the changeset to be saved via WP_Customize_Setting::save(). |
|
2863 $has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) ); |
|
2864 if ( $has_kses ) { |
|
2865 kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content. |
|
2866 } |
|
2867 |
|
2868 // Note that updating a post with publish status will trigger WP_Customize_Manager::publish_changeset_values(). |
|
2869 if ( $changeset_post_id ) { |
|
2870 if ( $args['autosave'] && 'auto-draft' !== get_post_status( $changeset_post_id ) ) { |
|
2871 // See _wp_translate_postdata() for why this is required as it will use the edit_post meta capability. |
|
2872 add_filter( 'map_meta_cap', array( $this, 'grant_edit_post_capability_for_changeset' ), 10, 4 ); |
|
2873 $post_array['post_ID'] = $post_array['ID']; |
|
2874 $post_array['post_type'] = 'customize_changeset'; |
|
2875 $r = wp_create_post_autosave( wp_slash( $post_array ) ); |
|
2876 remove_filter( 'map_meta_cap', array( $this, 'grant_edit_post_capability_for_changeset' ), 10 ); |
|
2877 } else { |
|
2878 $post_array['edit_date'] = true; // Prevent date clearing. |
|
2879 $r = wp_update_post( wp_slash( $post_array ), true ); |
|
2880 |
|
2881 // Delete autosave revision for user when the changeset is updated. |
|
2882 if ( ! empty( $args['user_id'] ) ) { |
|
2883 $autosave_draft = wp_get_post_autosave( $changeset_post_id, $args['user_id'] ); |
|
2884 if ( $autosave_draft ) { |
|
2885 wp_delete_post( $autosave_draft->ID, true ); |
|
2886 } |
|
2887 } |
|
2888 } |
|
2889 } else { |
|
2890 $r = wp_insert_post( wp_slash( $post_array ), true ); |
|
2891 if ( ! is_wp_error( $r ) ) { |
|
2892 $this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset. |
|
2893 } |
|
2894 } |
|
2895 if ( $has_kses ) { |
|
2896 kses_init_filters(); |
|
2897 } |
|
2898 $this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents. |
|
2899 |
|
2900 remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) ); |
|
2901 |
|
2902 $response = array( |
|
2903 'setting_validities' => $setting_validities, |
|
2904 ); |
|
2905 |
|
2906 if ( is_wp_error( $r ) ) { |
|
2907 $response['changeset_post_save_failure'] = $r->get_error_code(); |
|
2908 return new WP_Error( 'changeset_post_save_failure', '', $response ); |
|
2909 } |
|
2910 |
|
2911 return $response; |
|
2912 } |
|
2913 |
|
2914 /** |
|
2915 * Trash or delete a changeset post. |
|
2916 * |
|
2917 * The following re-formulates the logic from `wp_trash_post()` as done in |
|
2918 * `wp_publish_post()`. The reason for bypassing `wp_trash_post()` is that it |
|
2919 * will mutate the the `post_content` and the `post_name` when they should be |
|
2920 * untouched. |
|
2921 * |
|
2922 * @since 4.9.0 |
|
2923 * @global wpdb $wpdb WordPress database abstraction object. |
|
2924 * @see wp_trash_post() |
|
2925 * |
|
2926 * @param int|WP_Post $post The changeset post. |
|
2927 * @return mixed A WP_Post object for the trashed post or an empty value on failure. |
|
2928 */ |
|
2929 public function trash_changeset_post( $post ) { |
|
2930 global $wpdb; |
|
2931 |
|
2932 $post = get_post( $post ); |
|
2933 |
|
2934 if ( ! ( $post instanceof WP_Post ) ) { |
|
2935 return $post; |
|
2936 } |
|
2937 $post_id = $post->ID; |
|
2938 |
|
2939 if ( ! EMPTY_TRASH_DAYS ) { |
|
2940 return wp_delete_post( $post_id, true ); |
|
2941 } |
|
2942 |
|
2943 if ( 'trash' === get_post_status( $post ) ) { |
|
2944 return false; |
|
2945 } |
|
2946 |
|
2947 /** This filter is documented in wp-includes/post.php */ |
|
2948 $check = apply_filters( 'pre_trash_post', null, $post ); |
|
2949 if ( null !== $check ) { |
|
2950 return $check; |
|
2951 } |
|
2952 |
|
2953 /** This action is documented in wp-includes/post.php */ |
|
2954 do_action( 'wp_trash_post', $post_id ); |
|
2955 |
|
2956 add_post_meta( $post_id, '_wp_trash_meta_status', $post->post_status ); |
|
2957 add_post_meta( $post_id, '_wp_trash_meta_time', time() ); |
|
2958 |
|
2959 $old_status = $post->post_status; |
|
2960 $new_status = 'trash'; |
|
2961 $wpdb->update( $wpdb->posts, array( 'post_status' => $new_status ), array( 'ID' => $post->ID ) ); |
|
2962 clean_post_cache( $post->ID ); |
|
2963 |
|
2964 $post->post_status = $new_status; |
|
2965 wp_transition_post_status( $new_status, $old_status, $post ); |
|
2966 |
|
2967 /** This action is documented in wp-includes/post.php */ |
|
2968 do_action( 'edit_post', $post->ID, $post ); |
|
2969 |
|
2970 /** This action is documented in wp-includes/post.php */ |
|
2971 do_action( "save_post_{$post->post_type}", $post->ID, $post, true ); |
|
2972 |
|
2973 /** This action is documented in wp-includes/post.php */ |
|
2974 do_action( 'save_post', $post->ID, $post, true ); |
|
2975 |
|
2976 /** This action is documented in wp-includes/post.php */ |
|
2977 do_action( 'wp_insert_post', $post->ID, $post, true ); |
|
2978 |
|
2979 wp_trash_post_comments( $post_id ); |
|
2980 |
|
2981 /** This action is documented in wp-includes/post.php */ |
|
2982 do_action( 'trashed_post', $post_id ); |
|
2983 |
|
2984 return $post; |
|
2985 } |
|
2986 |
|
2987 /** |
|
2988 * Handle request to trash a changeset. |
|
2989 * |
|
2990 * @since 4.9.0 |
|
2991 */ |
|
2992 public function handle_changeset_trash_request() { |
|
2993 if ( ! is_user_logged_in() ) { |
|
2994 wp_send_json_error( 'unauthenticated' ); |
|
2995 } |
|
2996 |
|
2997 if ( ! $this->is_preview() ) { |
|
2998 wp_send_json_error( 'not_preview' ); |
|
2999 } |
|
3000 |
|
3001 if ( ! check_ajax_referer( 'trash_customize_changeset', 'nonce', false ) ) { |
|
3002 wp_send_json_error( array( |
|
3003 'code' => 'invalid_nonce', |
|
3004 'message' => __( 'There was an authentication problem. Please reload and try again.' ), |
|
3005 ) ); |
|
3006 } |
|
3007 |
|
3008 $changeset_post_id = $this->changeset_post_id(); |
|
3009 |
|
3010 if ( ! $changeset_post_id ) { |
|
3011 wp_send_json_error( array( |
|
3012 'message' => __( 'No changes saved yet, so there is nothing to trash.' ), |
|
3013 'code' => 'non_existent_changeset', |
|
3014 ) ); |
|
3015 return; |
|
3016 } |
|
3017 |
|
3018 if ( $changeset_post_id && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) { |
|
3019 wp_send_json_error( array( |
|
3020 'code' => 'changeset_trash_unauthorized', |
|
3021 'message' => __( 'Unable to trash changes.' ), |
|
3022 ) ); |
|
3023 } |
|
3024 |
|
3025 if ( 'trash' === get_post_status( $changeset_post_id ) ) { |
|
3026 wp_send_json_error( array( |
|
3027 'message' => __( 'Changes have already been trashed.' ), |
|
3028 'code' => 'changeset_already_trashed', |
|
3029 ) ); |
|
3030 return; |
|
3031 } |
|
3032 |
|
3033 $r = $this->trash_changeset_post( $changeset_post_id ); |
|
3034 if ( ! ( $r instanceof WP_Post ) ) { |
|
3035 wp_send_json_error( array( |
|
3036 'code' => 'changeset_trash_failure', |
|
3037 'message' => __( 'Unable to trash changes.' ), |
|
3038 ) ); |
|
3039 } |
|
3040 |
|
3041 wp_send_json_success( array( |
|
3042 'message' => __( 'Changes trashed successfully.' ), |
|
3043 ) ); |
|
3044 } |
|
3045 |
|
3046 /** |
|
3047 * Re-map 'edit_post' meta cap for a customize_changeset post to be the same as 'customize' maps. |
|
3048 * |
|
3049 * There is essentially a "meta meta" cap in play here, where 'edit_post' meta cap maps to |
|
3050 * the 'customize' meta cap which then maps to 'edit_theme_options'. This is currently |
|
3051 * required in core for `wp_create_post_autosave()` because it will call |
|
3052 * `_wp_translate_postdata()` which in turn will check if a user can 'edit_post', but the |
|
3053 * the caps for the customize_changeset post type are all mapping to the meta capability. |
|
3054 * This should be able to be removed once #40922 is addressed in core. |
|
3055 * |
|
3056 * @since 4.9.0 |
|
3057 * @link https://core.trac.wordpress.org/ticket/40922 |
|
3058 * @see WP_Customize_Manager::save_changeset_post() |
|
3059 * @see _wp_translate_postdata() |
|
3060 * |
|
3061 * @param array $caps Returns the user's actual capabilities. |
|
3062 * @param string $cap Capability name. |
|
3063 * @param int $user_id The user ID. |
|
3064 * @param array $args Adds the context to the cap. Typically the object ID. |
|
3065 * @return array Capabilities. |
|
3066 */ |
|
3067 public function grant_edit_post_capability_for_changeset( $caps, $cap, $user_id, $args ) { |
|
3068 if ( 'edit_post' === $cap && ! empty( $args[0] ) && 'customize_changeset' === get_post_type( $args[0] ) ) { |
|
3069 $post_type_obj = get_post_type_object( 'customize_changeset' ); |
|
3070 $caps = map_meta_cap( $post_type_obj->cap->$cap, $user_id ); |
|
3071 } |
|
3072 return $caps; |
|
3073 } |
|
3074 |
|
3075 /** |
|
3076 * Marks the changeset post as being currently edited by the current user. |
|
3077 * |
|
3078 * @since 4.9.0 |
|
3079 * |
|
3080 * @param int $changeset_post_id Changeset post id. |
|
3081 * @param bool $take_over Take over the changeset, default is false. |
|
3082 */ |
|
3083 public function set_changeset_lock( $changeset_post_id, $take_over = false ) { |
|
3084 if ( $changeset_post_id ) { |
|
3085 $can_override = ! (bool) get_post_meta( $changeset_post_id, '_edit_lock', true ); |
|
3086 |
|
3087 if ( $take_over ) { |
|
3088 $can_override = true; |
|
3089 } |
|
3090 |
|
3091 if ( $can_override ) { |
|
3092 $lock = sprintf( '%s:%s', time(), get_current_user_id() ); |
|
3093 update_post_meta( $changeset_post_id, '_edit_lock', $lock ); |
|
3094 } else { |
|
3095 $this->refresh_changeset_lock( $changeset_post_id ); |
|
3096 } |
|
3097 } |
|
3098 } |
|
3099 |
|
3100 /** |
|
3101 * Refreshes changeset lock with the current time if current user edited the changeset before. |
|
3102 * |
|
3103 * @since 4.9.0 |
|
3104 * |
|
3105 * @param int $changeset_post_id Changeset post id. |
|
3106 */ |
|
3107 public function refresh_changeset_lock( $changeset_post_id ) { |
|
3108 if ( ! $changeset_post_id ) { |
|
3109 return; |
|
3110 } |
|
3111 $lock = get_post_meta( $changeset_post_id, '_edit_lock', true ); |
|
3112 $lock = explode( ':', $lock ); |
|
3113 |
|
3114 if ( $lock && ! empty( $lock[1] ) ) { |
|
3115 $user_id = intval( $lock[1] ); |
|
3116 $current_user_id = get_current_user_id(); |
|
3117 if ( $user_id === $current_user_id ) { |
|
3118 $lock = sprintf( '%s:%s', time(), $user_id ); |
|
3119 update_post_meta( $changeset_post_id, '_edit_lock', $lock ); |
|
3120 } |
|
3121 } |
|
3122 } |
|
3123 |
|
3124 /** |
|
3125 * Filter heartbeat settings for the Customizer. |
|
3126 * |
|
3127 * @since 4.9.0 |
|
3128 * @param array $settings Current settings to filter. |
|
3129 * @return array Heartbeat settings. |
|
3130 */ |
|
3131 public function add_customize_screen_to_heartbeat_settings( $settings ) { |
|
3132 global $pagenow; |
|
3133 if ( 'customize.php' === $pagenow ) { |
|
3134 $settings['screenId'] = 'customize'; |
|
3135 } |
|
3136 return $settings; |
|
3137 } |
|
3138 |
|
3139 /** |
|
3140 * Get lock user data. |
|
3141 * |
|
3142 * @since 4.9.0 |
|
3143 * |
|
3144 * @param int $user_id User ID. |
|
3145 * @return array|null User data formatted for client. |
|
3146 */ |
|
3147 protected function get_lock_user_data( $user_id ) { |
|
3148 if ( ! $user_id ) { |
|
3149 return null; |
|
3150 } |
|
3151 $lock_user = get_userdata( $user_id ); |
|
3152 if ( ! $lock_user ) { |
|
3153 return null; |
|
3154 } |
|
3155 return array( |
|
3156 'id' => $lock_user->ID, |
|
3157 'name' => $lock_user->display_name, |
|
3158 'avatar' => get_avatar_url( $lock_user->ID, array( 'size' => 128 ) ), |
|
3159 ); |
|
3160 } |
|
3161 |
|
3162 /** |
|
3163 * Check locked changeset with heartbeat API. |
|
3164 * |
|
3165 * @since 4.9.0 |
|
3166 * |
|
3167 * @param array $response The Heartbeat response. |
|
3168 * @param array $data The $_POST data sent. |
|
3169 * @param string $screen_id The screen id. |
|
3170 * @return array The Heartbeat response. |
|
3171 */ |
|
3172 public function check_changeset_lock_with_heartbeat( $response, $data, $screen_id ) { |
|
3173 if ( isset( $data['changeset_uuid'] ) ) { |
|
3174 $changeset_post_id = $this->find_changeset_post_id( $data['changeset_uuid'] ); |
|
3175 } else { |
|
3176 $changeset_post_id = $this->changeset_post_id(); |
|
3177 } |
|
3178 |
|
3179 if ( |
|
3180 array_key_exists( 'check_changeset_lock', $data ) |
|
3181 && 'customize' === $screen_id |
|
3182 && $changeset_post_id |
|
3183 && current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) |
|
3184 ) { |
|
3185 $lock_user_id = wp_check_post_lock( $changeset_post_id ); |
|
3186 |
|
3187 if ( $lock_user_id ) { |
|
3188 $response['customize_changeset_lock_user'] = $this->get_lock_user_data( $lock_user_id ); |
|
3189 } else { |
|
3190 |
|
3191 // Refreshing time will ensure that the user is sitting on customizer and has not closed the customizer tab. |
|
3192 $this->refresh_changeset_lock( $changeset_post_id ); |
|
3193 } |
|
3194 } |
|
3195 |
|
3196 return $response; |
|
3197 } |
|
3198 |
|
3199 /** |
|
3200 * Removes changeset lock when take over request is sent via Ajax. |
|
3201 * |
|
3202 * @since 4.9.0 |
|
3203 */ |
|
3204 public function handle_override_changeset_lock_request() { |
|
3205 if ( ! $this->is_preview() ) { |
|
3206 wp_send_json_error( 'not_preview', 400 ); |
|
3207 } |
|
3208 |
|
3209 if ( ! check_ajax_referer( 'customize_override_changeset_lock', 'nonce', false ) ) { |
|
3210 wp_send_json_error( array( |
|
3211 'code' => 'invalid_nonce', |
|
3212 'message' => __( 'Security check failed.' ), |
|
3213 ) ); |
|
3214 } |
|
3215 |
|
3216 $changeset_post_id = $this->changeset_post_id(); |
|
3217 |
|
3218 if ( empty( $changeset_post_id ) ) { |
|
3219 wp_send_json_error( array( |
|
3220 'code' => 'no_changeset_found_to_take_over', |
|
3221 'message' => __( 'No changeset found to take over' ), |
|
3222 ) ); |
|
3223 } |
|
3224 |
|
3225 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) { |
|
3226 wp_send_json_error( array( |
|
3227 'code' => 'cannot_remove_changeset_lock', |
|
3228 'message' => __( 'Sorry, you are not allowed to take over.' ), |
|
3229 ) ); |
|
3230 } |
|
3231 |
|
3232 $this->set_changeset_lock( $changeset_post_id, true ); |
|
3233 |
|
3234 wp_send_json_success( 'changeset_taken_over' ); |
|
3235 } |
|
3236 |
|
3237 /** |
|
3238 * Whether a changeset revision should be made. |
|
3239 * |
|
3240 * @since 4.7.0 |
|
3241 * @var bool |
|
3242 */ |
|
3243 protected $store_changeset_revision; |
|
3244 |
|
3245 /** |
|
3246 * Filters whether a changeset has changed to create a new revision. |
|
3247 * |
|
3248 * Note that this will not be called while a changeset post remains in auto-draft status. |
|
3249 * |
|
3250 * @since 4.7.0 |
|
3251 * |
|
3252 * @param bool $post_has_changed Whether the post has changed. |
|
3253 * @param WP_Post $last_revision The last revision post object. |
|
3254 * @param WP_Post $post The post object. |
|
3255 * |
|
3256 * @return bool Whether a revision should be made. |
|
3257 */ |
|
3258 public function _filter_revision_post_has_changed( $post_has_changed, $last_revision, $post ) { |
|
3259 unset( $last_revision ); |
|
3260 if ( 'customize_changeset' === $post->post_type ) { |
|
3261 $post_has_changed = $this->store_changeset_revision; |
|
3262 } |
|
3263 return $post_has_changed; |
|
3264 } |
|
3265 |
|
3266 /** |
|
3267 * Publish changeset values. |
|
3268 * |
|
3269 * This will the values contained in a changeset, even changesets that do not |
|
3270 * correspond to current manager instance. This is called by |
|
3271 * `_wp_customize_publish_changeset()` when a customize_changeset post is |
|
3272 * transitioned to the `publish` status. As such, this method should not be |
|
3273 * called directly and instead `wp_publish_post()` should be used. |
|
3274 * |
|
3275 * Please note that if the settings in the changeset are for a non-activated |
|
3276 * theme, the theme must first be switched to (via `switch_theme()`) before |
|
3277 * invoking this method. |
|
3278 * |
|
3279 * @since 4.7.0 |
|
3280 * @see _wp_customize_publish_changeset() |
|
3281 * @global wpdb $wpdb |
|
3282 * |
|
3283 * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance. |
|
3284 * @return true|WP_Error True or error info. |
|
3285 */ |
|
3286 public function _publish_changeset_values( $changeset_post_id ) { |
|
3287 global $wpdb; |
|
3288 |
|
3289 $publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id ); |
|
3290 if ( is_wp_error( $publishing_changeset_data ) ) { |
|
3291 return $publishing_changeset_data; |
|
3292 } |
|
3293 |
|
3294 $changeset_post = get_post( $changeset_post_id ); |
|
3295 |
|
3296 /* |
|
3297 * Temporarily override the changeset context so that it will be read |
|
3298 * in calls to unsanitized_post_values() and so that it will be available |
|
3299 * on the $wp_customize object passed to hooks during the save logic. |
|
3300 */ |
|
3301 $previous_changeset_post_id = $this->_changeset_post_id; |
|
3302 $this->_changeset_post_id = $changeset_post_id; |
|
3303 $previous_changeset_uuid = $this->_changeset_uuid; |
|
3304 $this->_changeset_uuid = $changeset_post->post_name; |
|
3305 $previous_changeset_data = $this->_changeset_data; |
|
3306 $this->_changeset_data = $publishing_changeset_data; |
|
3307 |
|
3308 // Parse changeset data to identify theme mod settings and user IDs associated with settings to be saved. |
|
3309 $setting_user_ids = array(); |
|
3310 $theme_mod_settings = array(); |
|
3311 $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/'; |
|
3312 $matches = array(); |
|
3313 foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) { |
|
3314 $actual_setting_id = null; |
|
3315 $is_theme_mod_setting = ( |
|
3316 isset( $setting_params['value'] ) |
|
3317 && |
|
3318 isset( $setting_params['type'] ) |
|
3319 && |
|
3320 'theme_mod' === $setting_params['type'] |
|
3321 && |
|
3322 preg_match( $namespace_pattern, $raw_setting_id, $matches ) |
|
3323 ); |
|
3324 if ( $is_theme_mod_setting ) { |
|
3325 if ( ! isset( $theme_mod_settings[ $matches['stylesheet'] ] ) ) { |
|
3326 $theme_mod_settings[ $matches['stylesheet'] ] = array(); |
|
3327 } |
|
3328 $theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params; |
|
3329 |
|
3330 if ( $this->get_stylesheet() === $matches['stylesheet'] ) { |
|
3331 $actual_setting_id = $matches['setting_id']; |
|
3332 } |
|
3333 } else { |
|
3334 $actual_setting_id = $raw_setting_id; |
|
3335 } |
|
3336 |
|
3337 // Keep track of the user IDs for settings actually for this theme. |
|
3338 if ( $actual_setting_id && isset( $setting_params['user_id'] ) ) { |
|
3339 $setting_user_ids[ $actual_setting_id ] = $setting_params['user_id']; |
|
3340 } |
|
3341 } |
|
3342 |
|
3343 $changeset_setting_values = $this->unsanitized_post_values( array( |
|
3344 'exclude_post_data' => true, |
|
3345 'exclude_changeset' => false, |
|
3346 ) ); |
|
3347 $changeset_setting_ids = array_keys( $changeset_setting_values ); |
|
3348 $this->add_dynamic_settings( $changeset_setting_ids ); |
748 |
3349 |
749 /** |
3350 /** |
750 * Fires once the theme has switched in the Customizer, but before settings |
3351 * Fires once the theme has switched in the Customizer, but before settings |
751 * have been saved. |
3352 * have been saved. |
752 * |
3353 * |
753 * @since 3.4.0 |
3354 * @since 3.4.0 |
754 * |
3355 * |
755 * @param WP_Customize_Manager $this WP_Customize_Manager instance. |
3356 * @param WP_Customize_Manager $manager WP_Customize_Manager instance. |
756 */ |
3357 */ |
757 do_action( 'customize_save', $this ); |
3358 do_action( 'customize_save', $this ); |
758 |
3359 |
759 foreach ( $this->settings as $setting ) { |
3360 /* |
760 $setting->save(); |
3361 * Ensure that all settings will allow themselves to be saved. Note that |
|
3362 * this is safe because the setting would have checked the capability |
|
3363 * when the setting value was written into the changeset. So this is why |
|
3364 * an additional capability check is not required here. |
|
3365 */ |
|
3366 $original_setting_capabilities = array(); |
|
3367 foreach ( $changeset_setting_ids as $setting_id ) { |
|
3368 $setting = $this->get_setting( $setting_id ); |
|
3369 if ( $setting && ! isset( $setting_user_ids[ $setting_id ] ) ) { |
|
3370 $original_setting_capabilities[ $setting->id ] = $setting->capability; |
|
3371 $setting->capability = 'exist'; |
|
3372 } |
|
3373 } |
|
3374 |
|
3375 $original_user_id = get_current_user_id(); |
|
3376 foreach ( $changeset_setting_ids as $setting_id ) { |
|
3377 $setting = $this->get_setting( $setting_id ); |
|
3378 if ( $setting ) { |
|
3379 /* |
|
3380 * Set the current user to match the user who saved the value into |
|
3381 * the changeset so that any filters that apply during the save |
|
3382 * process will respect the original user's capabilities. This |
|
3383 * will ensure, for example, that KSES won't strip unsafe HTML |
|
3384 * when a scheduled changeset publishes via WP Cron. |
|
3385 */ |
|
3386 if ( isset( $setting_user_ids[ $setting_id ] ) ) { |
|
3387 wp_set_current_user( $setting_user_ids[ $setting_id ] ); |
|
3388 } else { |
|
3389 wp_set_current_user( $original_user_id ); |
|
3390 } |
|
3391 |
|
3392 $setting->save(); |
|
3393 } |
|
3394 } |
|
3395 wp_set_current_user( $original_user_id ); |
|
3396 |
|
3397 // Update the stashed theme mod settings, removing the active theme's stashed settings, if activated. |
|
3398 if ( did_action( 'switch_theme' ) ) { |
|
3399 $other_theme_mod_settings = $theme_mod_settings; |
|
3400 unset( $other_theme_mod_settings[ $this->get_stylesheet() ] ); |
|
3401 $this->update_stashed_theme_mod_settings( $other_theme_mod_settings ); |
761 } |
3402 } |
762 |
3403 |
763 /** |
3404 /** |
764 * Fires after Customize settings have been saved. |
3405 * Fires after Customize settings have been saved. |
765 * |
3406 * |
766 * @since 3.6.0 |
3407 * @since 3.6.0 |
767 * |
3408 * |
768 * @param WP_Customize_Manager $this WP_Customize_Manager instance. |
3409 * @param WP_Customize_Manager $manager WP_Customize_Manager instance. |
769 */ |
3410 */ |
770 do_action( 'customize_save_after', $this ); |
3411 do_action( 'customize_save_after', $this ); |
771 |
3412 |
772 /** |
3413 // Restore original capabilities. |
773 * Filter response data for a successful customize_save AJAX request. |
3414 foreach ( $original_setting_capabilities as $setting_id => $capability ) { |
774 * |
3415 $setting = $this->get_setting( $setting_id ); |
775 * This filter does not apply if there was a nonce or authentication failure. |
3416 if ( $setting ) { |
776 * |
3417 $setting->capability = $capability; |
777 * @since 4.2.0 |
3418 } |
778 * |
3419 } |
779 * @param array $data Additional information passed back to the 'saved' |
3420 |
780 * event on `wp.customize`. |
3421 // Restore original changeset data. |
781 * @param WP_Customize_Manager $this WP_Customize_Manager instance. |
3422 $this->_changeset_data = $previous_changeset_data; |
|
3423 $this->_changeset_post_id = $previous_changeset_post_id; |
|
3424 $this->_changeset_uuid = $previous_changeset_uuid; |
|
3425 |
|
3426 /* |
|
3427 * Convert all autosave revisions into their own auto-drafts so that users can be prompted to |
|
3428 * restore them when a changeset is published, but they had been locked out from including |
|
3429 * their changes in the changeset. |
782 */ |
3430 */ |
783 $response = apply_filters( 'customize_save_response', array(), $this ); |
3431 $revisions = wp_get_post_revisions( $changeset_post_id, array( 'check_enabled' => false ) ); |
784 wp_send_json_success( $response ); |
3432 foreach ( $revisions as $revision ) { |
|
3433 if ( false !== strpos( $revision->post_name, "{$changeset_post_id}-autosave" ) ) { |
|
3434 $wpdb->update( |
|
3435 $wpdb->posts, |
|
3436 array( |
|
3437 'post_status' => 'auto-draft', |
|
3438 'post_type' => 'customize_changeset', |
|
3439 'post_name' => wp_generate_uuid4(), |
|
3440 'post_parent' => 0, |
|
3441 ), |
|
3442 array( |
|
3443 'ID' => $revision->ID, |
|
3444 ) |
|
3445 ); |
|
3446 clean_post_cache( $revision->ID ); |
|
3447 } |
|
3448 } |
|
3449 |
|
3450 return true; |
|
3451 } |
|
3452 |
|
3453 /** |
|
3454 * Update stashed theme mod settings. |
|
3455 * |
|
3456 * @since 4.7.0 |
|
3457 * |
|
3458 * @param array $inactive_theme_mod_settings Mapping of stylesheet to arrays of theme mod settings. |
|
3459 * @return array|false Returns array of updated stashed theme mods or false if the update failed or there were no changes. |
|
3460 */ |
|
3461 protected function update_stashed_theme_mod_settings( $inactive_theme_mod_settings ) { |
|
3462 $stashed_theme_mod_settings = get_option( 'customize_stashed_theme_mods' ); |
|
3463 if ( empty( $stashed_theme_mod_settings ) ) { |
|
3464 $stashed_theme_mod_settings = array(); |
|
3465 } |
|
3466 |
|
3467 // Delete any stashed theme mods for the active theme since they would have been loaded and saved upon activation. |
|
3468 unset( $stashed_theme_mod_settings[ $this->get_stylesheet() ] ); |
|
3469 |
|
3470 // Merge inactive theme mods with the stashed theme mod settings. |
|
3471 foreach ( $inactive_theme_mod_settings as $stylesheet => $theme_mod_settings ) { |
|
3472 if ( ! isset( $stashed_theme_mod_settings[ $stylesheet ] ) ) { |
|
3473 $stashed_theme_mod_settings[ $stylesheet ] = array(); |
|
3474 } |
|
3475 |
|
3476 $stashed_theme_mod_settings[ $stylesheet ] = array_merge( |
|
3477 $stashed_theme_mod_settings[ $stylesheet ], |
|
3478 $theme_mod_settings |
|
3479 ); |
|
3480 } |
|
3481 |
|
3482 $autoload = false; |
|
3483 $result = update_option( 'customize_stashed_theme_mods', $stashed_theme_mod_settings, $autoload ); |
|
3484 if ( ! $result ) { |
|
3485 return false; |
|
3486 } |
|
3487 return $stashed_theme_mod_settings; |
785 } |
3488 } |
786 |
3489 |
787 /** |
3490 /** |
788 * Refresh nonces for the current preview. |
3491 * Refresh nonces for the current preview. |
789 * |
3492 * |
1049 * Register a customize control type. |
3946 * Register a customize control type. |
1050 * |
3947 * |
1051 * Registered types are eligible to be rendered via JS and created dynamically. |
3948 * Registered types are eligible to be rendered via JS and created dynamically. |
1052 * |
3949 * |
1053 * @since 4.1.0 |
3950 * @since 4.1.0 |
1054 * @access public |
|
1055 * |
3951 * |
1056 * @param string $control Name of a custom control which is a subclass of |
3952 * @param string $control Name of a custom control which is a subclass of |
1057 * {@see WP_Customize_Control}. |
3953 * WP_Customize_Control. |
1058 */ |
3954 */ |
1059 public function register_control_type( $control ) { |
3955 public function register_control_type( $control ) { |
1060 $this->registered_control_types[] = $control; |
3956 $this->registered_control_types[] = $control; |
1061 } |
3957 } |
1062 |
3958 |
1063 /** |
3959 /** |
1064 * Render JS templates for all registered control types. |
3960 * Render JS templates for all registered control types. |
1065 * |
3961 * |
1066 * @since 4.1.0 |
3962 * @since 4.1.0 |
1067 * @access public |
|
1068 */ |
3963 */ |
1069 public function render_control_templates() { |
3964 public function render_control_templates() { |
|
3965 if ( $this->branching() ) { |
|
3966 $l10n = array( |
|
3967 /* translators: %s: User who is customizing the changeset in customizer. */ |
|
3968 'locked' => __( '%s is already customizing this changeset. Please wait until they are done to try customizing. Your latest changes have been autosaved.' ), |
|
3969 /* translators: %s: User who is customizing the changeset in customizer. */ |
|
3970 'locked_allow_override' => __( '%s is already customizing this changeset. Do you want to take over?' ), |
|
3971 ); |
|
3972 } else { |
|
3973 $l10n = array( |
|
3974 /* translators: %s: User who is customizing the changeset in customizer. */ |
|
3975 'locked' => __( '%s is already customizing this site. Please wait until they are done to try customizing. Your latest changes have been autosaved.' ), |
|
3976 /* translators: %s: User who is customizing the changeset in customizer. */ |
|
3977 'locked_allow_override' => __( '%s is already customizing this site. Do you want to take over?' ), |
|
3978 ); |
|
3979 } |
|
3980 |
1070 foreach ( $this->registered_control_types as $control_type ) { |
3981 foreach ( $this->registered_control_types as $control_type ) { |
1071 $control = new $control_type( $this, 'temp', array() ); |
3982 $control = new $control_type( $this, 'temp', array( |
|
3983 'settings' => array(), |
|
3984 ) ); |
1072 $control->print_template(); |
3985 $control->print_template(); |
1073 } |
3986 } |
|
3987 ?> |
|
3988 |
|
3989 <script type="text/html" id="tmpl-customize-control-default-content"> |
|
3990 <# |
|
3991 var inputId = _.uniqueId( 'customize-control-default-input-' ); |
|
3992 var descriptionId = _.uniqueId( 'customize-control-default-description-' ); |
|
3993 var describedByAttr = data.description ? ' aria-describedby="' + descriptionId + '" ' : ''; |
|
3994 #> |
|
3995 <# switch ( data.type ) { |
|
3996 case 'checkbox': #> |
|
3997 <span class="customize-inside-control-row"> |
|
3998 <input |
|
3999 id="{{ inputId }}" |
|
4000 {{{ describedByAttr }}} |
|
4001 type="checkbox" |
|
4002 value="{{ data.value }}" |
|
4003 data-customize-setting-key-link="default" |
|
4004 > |
|
4005 <label for="{{ inputId }}"> |
|
4006 {{ data.label }} |
|
4007 </label> |
|
4008 <# if ( data.description ) { #> |
|
4009 <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span> |
|
4010 <# } #> |
|
4011 </span> |
|
4012 <# |
|
4013 break; |
|
4014 case 'radio': |
|
4015 if ( ! data.choices ) { |
|
4016 return; |
|
4017 } |
|
4018 #> |
|
4019 <# if ( data.label ) { #> |
|
4020 <label for="{{ inputId }}" class="customize-control-title"> |
|
4021 {{ data.label }} |
|
4022 </label> |
|
4023 <# } #> |
|
4024 <# if ( data.description ) { #> |
|
4025 <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span> |
|
4026 <# } #> |
|
4027 <# _.each( data.choices, function( val, key ) { #> |
|
4028 <span class="customize-inside-control-row"> |
|
4029 <# |
|
4030 var value, text; |
|
4031 if ( _.isObject( val ) ) { |
|
4032 value = val.value; |
|
4033 text = val.text; |
|
4034 } else { |
|
4035 value = key; |
|
4036 text = val; |
|
4037 } |
|
4038 #> |
|
4039 <input |
|
4040 id="{{ inputId + '-' + value }}" |
|
4041 type="radio" |
|
4042 value="{{ value }}" |
|
4043 name="{{ inputId }}" |
|
4044 data-customize-setting-key-link="default" |
|
4045 {{{ describedByAttr }}} |
|
4046 > |
|
4047 <label for="{{ inputId + '-' + value }}">{{ text }}</label> |
|
4048 </span> |
|
4049 <# } ); #> |
|
4050 <# |
|
4051 break; |
|
4052 default: |
|
4053 #> |
|
4054 <# if ( data.label ) { #> |
|
4055 <label for="{{ inputId }}" class="customize-control-title"> |
|
4056 {{ data.label }} |
|
4057 </label> |
|
4058 <# } #> |
|
4059 <# if ( data.description ) { #> |
|
4060 <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span> |
|
4061 <# } #> |
|
4062 |
|
4063 <# |
|
4064 var inputAttrs = { |
|
4065 id: inputId, |
|
4066 'data-customize-setting-key-link': 'default' |
|
4067 }; |
|
4068 if ( 'textarea' === data.type ) { |
|
4069 inputAttrs.rows = '5'; |
|
4070 } else if ( 'button' === data.type ) { |
|
4071 inputAttrs['class'] = 'button button-secondary'; |
|
4072 inputAttrs.type = 'button'; |
|
4073 } else { |
|
4074 inputAttrs.type = data.type; |
|
4075 } |
|
4076 if ( data.description ) { |
|
4077 inputAttrs['aria-describedby'] = descriptionId; |
|
4078 } |
|
4079 _.extend( inputAttrs, data.input_attrs ); |
|
4080 #> |
|
4081 |
|
4082 <# if ( 'button' === data.type ) { #> |
|
4083 <button |
|
4084 <# _.each( _.extend( inputAttrs ), function( value, key ) { #> |
|
4085 {{{ key }}}="{{ value }}" |
|
4086 <# } ); #> |
|
4087 >{{ inputAttrs.value }}</button> |
|
4088 <# } else if ( 'textarea' === data.type ) { #> |
|
4089 <textarea |
|
4090 <# _.each( _.extend( inputAttrs ), function( value, key ) { #> |
|
4091 {{{ key }}}="{{ value }}" |
|
4092 <# }); #> |
|
4093 >{{ inputAttrs.value }}</textarea> |
|
4094 <# } else if ( 'select' === data.type ) { #> |
|
4095 <# delete inputAttrs.type; #> |
|
4096 <select |
|
4097 <# _.each( _.extend( inputAttrs ), function( value, key ) { #> |
|
4098 {{{ key }}}="{{ value }}" |
|
4099 <# }); #> |
|
4100 > |
|
4101 <# _.each( data.choices, function( val, key ) { #> |
|
4102 <# |
|
4103 var value, text; |
|
4104 if ( _.isObject( val ) ) { |
|
4105 value = val.value; |
|
4106 text = val.text; |
|
4107 } else { |
|
4108 value = key; |
|
4109 text = val; |
|
4110 } |
|
4111 #> |
|
4112 <option value="{{ value }}">{{ text }}</option> |
|
4113 <# } ); #> |
|
4114 </select> |
|
4115 <# } else { #> |
|
4116 <input |
|
4117 <# _.each( _.extend( inputAttrs ), function( value, key ) { #> |
|
4118 {{{ key }}}="{{ value }}" |
|
4119 <# }); #> |
|
4120 > |
|
4121 <# } #> |
|
4122 <# } #> |
|
4123 </script> |
|
4124 |
|
4125 <script type="text/html" id="tmpl-customize-notification"> |
|
4126 <li class="notice notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}"> |
|
4127 <div class="notification-message">{{{ data.message || data.code }}}</div> |
|
4128 <# if ( data.dismissible ) { #> |
|
4129 <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php _e( 'Dismiss' ); ?></span></button> |
|
4130 <# } #> |
|
4131 </li> |
|
4132 </script> |
|
4133 |
|
4134 <script type="text/html" id="tmpl-customize-changeset-locked-notification"> |
|
4135 <li class="notice notice-{{ data.type || 'info' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}"> |
|
4136 <div class="notification-message customize-changeset-locked-message"> |
|
4137 <img class="customize-changeset-locked-avatar" src="{{ data.lockUser.avatar }}" alt="{{ data.lockUser.name }}"> |
|
4138 <p class="currently-editing"> |
|
4139 <# if ( data.message ) { #> |
|
4140 {{{ data.message }}} |
|
4141 <# } else if ( data.allowOverride ) { #> |
|
4142 <?php |
|
4143 echo esc_html( sprintf( $l10n['locked_allow_override'], '{{ data.lockUser.name }}' ) ); |
|
4144 ?> |
|
4145 <# } else { #> |
|
4146 <?php |
|
4147 echo esc_html( sprintf( $l10n['locked'], '{{ data.lockUser.name }}' ) ); |
|
4148 ?> |
|
4149 <# } #> |
|
4150 </p> |
|
4151 <p class="notice notice-error notice-alt" hidden></p> |
|
4152 <p class="action-buttons"> |
|
4153 <# if ( data.returnUrl !== data.previewUrl ) { #> |
|
4154 <a class="button customize-notice-go-back-button" href="{{ data.returnUrl }}"><?php _e( 'Go back' ); ?></a> |
|
4155 <# } #> |
|
4156 <a class="button customize-notice-preview-button" href="{{ data.frontendPreviewUrl }}"><?php _e( 'Preview' ); ?></a> |
|
4157 <# if ( data.allowOverride ) { #> |
|
4158 <button class="button button-primary wp-tab-last customize-notice-take-over-button"><?php _e( 'Take over' ); ?></button> |
|
4159 <# } #> |
|
4160 </p> |
|
4161 </div> |
|
4162 </li> |
|
4163 </script> |
|
4164 |
|
4165 <script type="text/html" id="tmpl-customize-code-editor-lint-error-notification"> |
|
4166 <li class="notice notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}"> |
|
4167 <div class="notification-message">{{{ data.message || data.code }}}</div> |
|
4168 |
|
4169 <p> |
|
4170 <# var elementId = 'el-' + String( Math.random() ); #> |
|
4171 <input id="{{ elementId }}" type="checkbox"> |
|
4172 <label for="{{ elementId }}"><?php _e( 'Update anyway, even though it might break your site?' ); ?></label> |
|
4173 </p> |
|
4174 </li> |
|
4175 </script> |
|
4176 |
|
4177 <?php |
|
4178 /* The following template is obsolete in core but retained for plugins. */ |
|
4179 ?> |
|
4180 <script type="text/html" id="tmpl-customize-control-notifications"> |
|
4181 <ul> |
|
4182 <# _.each( data.notifications, function( notification ) { #> |
|
4183 <li class="notice notice-{{ notification.type || 'info' }} {{ data.altNotice ? 'notice-alt' : '' }}" data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{{ notification.message || notification.code }}}</li> |
|
4184 <# } ); #> |
|
4185 </ul> |
|
4186 </script> |
|
4187 |
|
4188 <script type="text/html" id="tmpl-customize-preview-link-control" > |
|
4189 <# var elementPrefix = _.uniqueId( 'el' ) + '-' #> |
|
4190 <p class="customize-control-title"> |
|
4191 <?php esc_html_e( 'Share Preview Link' ); ?> |
|
4192 </p> |
|
4193 <p class="description customize-control-description"><?php esc_html_e( 'See how changes would look live on your website, and share the preview with people who can\'t access the Customizer.' ); ?></p> |
|
4194 <div class="customize-control-notifications-container"></div> |
|
4195 <div class="preview-link-wrapper"> |
|
4196 <label for="{{ elementPrefix }}customize-preview-link-input" class="screen-reader-text"><?php esc_html_e( 'Preview Link' ); ?></label> |
|
4197 <a href="" target=""> |
|
4198 <span class="preview-control-element" data-component="url"></span> |
|
4199 <span class="screen-reader-text"><?php _e( '(opens in a new window)' ); ?></span> |
|
4200 </a> |
|
4201 <input id="{{ elementPrefix }}customize-preview-link-input" readonly tabindex="-1" class="preview-control-element" data-component="input"> |
|
4202 <button class="customize-copy-preview-link preview-control-element button button-secondary" data-component="button" data-copy-text="<?php esc_attr_e( 'Copy' ); ?>" data-copied-text="<?php esc_attr_e( 'Copied' ); ?>" ><?php esc_html_e( 'Copy' ); ?></button> |
|
4203 </div> |
|
4204 </script> |
|
4205 <script type="text/html" id="tmpl-customize-selected-changeset-status-control"> |
|
4206 <# var inputId = _.uniqueId( 'customize-selected-changeset-status-control-input-' ); #> |
|
4207 <# var descriptionId = _.uniqueId( 'customize-selected-changeset-status-control-description-' ); #> |
|
4208 <# if ( data.label ) { #> |
|
4209 <label for="{{ inputId }}" class="customize-control-title">{{ data.label }}</label> |
|
4210 <# } #> |
|
4211 <# if ( data.description ) { #> |
|
4212 <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span> |
|
4213 <# } #> |
|
4214 <# _.each( data.choices, function( choice ) { #> |
|
4215 <# var choiceId = inputId + '-' + choice.status; #> |
|
4216 <span class="customize-inside-control-row"> |
|
4217 <input id="{{ choiceId }}" type="radio" value="{{ choice.status }}" name="{{ inputId }}" data-customize-setting-key-link="default"> |
|
4218 <label for="{{ choiceId }}">{{ choice.label }}</label> |
|
4219 </span> |
|
4220 <# } ); #> |
|
4221 </script> |
|
4222 <?php |
1074 } |
4223 } |
1075 |
4224 |
1076 /** |
4225 /** |
1077 * Helper function to compare two objects by priority, ensuring sort stability via instance_number. |
4226 * Helper function to compare two objects by priority, ensuring sort stability via instance_number. |
1078 * |
4227 * |
1079 * @since 3.4.0 |
4228 * @since 3.4.0 |
|
4229 * @deprecated 4.7.0 Use wp_list_sort() |
1080 * |
4230 * |
1081 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $a Object A. |
4231 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $a Object A. |
1082 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $b Object B. |
4232 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $b Object B. |
1083 * @return int |
4233 * @return int |
1084 */ |
4234 */ |
1085 protected function _cmp_priority( $a, $b ) { |
4235 protected function _cmp_priority( $a, $b ) { |
|
4236 _deprecated_function( __METHOD__, '4.7.0', 'wp_list_sort' ); |
|
4237 |
1086 if ( $a->priority === $b->priority ) { |
4238 if ( $a->priority === $b->priority ) { |
1087 return $a->instance_number - $a->instance_number; |
4239 return $a->instance_number - $b->instance_number; |
1088 } else { |
4240 } else { |
1089 return $a->priority - $b->priority; |
4241 return $a->priority - $b->priority; |
1090 } |
4242 } |
1091 } |
4243 } |
1092 |
4244 |
1163 */ |
4334 */ |
1164 public function enqueue_control_scripts() { |
4335 public function enqueue_control_scripts() { |
1165 foreach ( $this->controls as $control ) { |
4336 foreach ( $this->controls as $control ) { |
1166 $control->enqueue(); |
4337 $control->enqueue(); |
1167 } |
4338 } |
|
4339 |
|
4340 if ( ! is_multisite() && ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) || current_user_can( 'delete_themes' ) ) ) { |
|
4341 wp_enqueue_script( 'updates' ); |
|
4342 wp_localize_script( 'updates', '_wpUpdatesItemCounts', array( |
|
4343 'totals' => wp_get_update_data(), |
|
4344 ) ); |
|
4345 } |
|
4346 } |
|
4347 |
|
4348 /** |
|
4349 * Determine whether the user agent is iOS. |
|
4350 * |
|
4351 * @since 4.4.0 |
|
4352 * |
|
4353 * @return bool Whether the user agent is iOS. |
|
4354 */ |
|
4355 public function is_ios() { |
|
4356 return wp_is_mobile() && preg_match( '/iPad|iPod|iPhone/', $_SERVER['HTTP_USER_AGENT'] ); |
|
4357 } |
|
4358 |
|
4359 /** |
|
4360 * Get the template string for the Customizer pane document title. |
|
4361 * |
|
4362 * @since 4.4.0 |
|
4363 * |
|
4364 * @return string The template string for the document title. |
|
4365 */ |
|
4366 public function get_document_title_template() { |
|
4367 if ( $this->is_theme_active() ) { |
|
4368 /* translators: %s: document title from the preview */ |
|
4369 $document_title_tmpl = __( 'Customize: %s' ); |
|
4370 } else { |
|
4371 /* translators: %s: document title from the preview */ |
|
4372 $document_title_tmpl = __( 'Live Preview: %s' ); |
|
4373 } |
|
4374 $document_title_tmpl = html_entity_decode( $document_title_tmpl, ENT_QUOTES, 'UTF-8' ); // Because exported to JS and assigned to document.title. |
|
4375 return $document_title_tmpl; |
|
4376 } |
|
4377 |
|
4378 /** |
|
4379 * Set the initial URL to be previewed. |
|
4380 * |
|
4381 * URL is validated. |
|
4382 * |
|
4383 * @since 4.4.0 |
|
4384 * |
|
4385 * @param string $preview_url URL to be previewed. |
|
4386 */ |
|
4387 public function set_preview_url( $preview_url ) { |
|
4388 $preview_url = esc_url_raw( $preview_url ); |
|
4389 $this->preview_url = wp_validate_redirect( $preview_url, home_url( '/' ) ); |
|
4390 } |
|
4391 |
|
4392 /** |
|
4393 * Get the initial URL to be previewed. |
|
4394 * |
|
4395 * @since 4.4.0 |
|
4396 * |
|
4397 * @return string URL being previewed. |
|
4398 */ |
|
4399 public function get_preview_url() { |
|
4400 if ( empty( $this->preview_url ) ) { |
|
4401 $preview_url = home_url( '/' ); |
|
4402 } else { |
|
4403 $preview_url = $this->preview_url; |
|
4404 } |
|
4405 return $preview_url; |
|
4406 } |
|
4407 |
|
4408 /** |
|
4409 * Determines whether the admin and the frontend are on different domains. |
|
4410 * |
|
4411 * @since 4.7.0 |
|
4412 * |
|
4413 * @return bool Whether cross-domain. |
|
4414 */ |
|
4415 public function is_cross_domain() { |
|
4416 $admin_origin = wp_parse_url( admin_url() ); |
|
4417 $home_origin = wp_parse_url( home_url() ); |
|
4418 $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) ); |
|
4419 return $cross_domain; |
|
4420 } |
|
4421 |
|
4422 /** |
|
4423 * Get URLs allowed to be previewed. |
|
4424 * |
|
4425 * If the front end and the admin are served from the same domain, load the |
|
4426 * preview over ssl if the Customizer is being loaded over ssl. This avoids |
|
4427 * insecure content warnings. This is not attempted if the admin and front end |
|
4428 * are on different domains to avoid the case where the front end doesn't have |
|
4429 * ssl certs. Domain mapping plugins can allow other urls in these conditions |
|
4430 * using the customize_allowed_urls filter. |
|
4431 * |
|
4432 * @since 4.7.0 |
|
4433 * |
|
4434 * @returns array Allowed URLs. |
|
4435 */ |
|
4436 public function get_allowed_urls() { |
|
4437 $allowed_urls = array( home_url( '/' ) ); |
|
4438 |
|
4439 if ( is_ssl() && ! $this->is_cross_domain() ) { |
|
4440 $allowed_urls[] = home_url( '/', 'https' ); |
|
4441 } |
|
4442 |
|
4443 /** |
|
4444 * Filters the list of URLs allowed to be clicked and followed in the Customizer preview. |
|
4445 * |
|
4446 * @since 3.4.0 |
|
4447 * |
|
4448 * @param array $allowed_urls An array of allowed URLs. |
|
4449 */ |
|
4450 $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) ); |
|
4451 |
|
4452 return $allowed_urls; |
|
4453 } |
|
4454 |
|
4455 /** |
|
4456 * Get messenger channel. |
|
4457 * |
|
4458 * @since 4.7.0 |
|
4459 * |
|
4460 * @return string Messenger channel. |
|
4461 */ |
|
4462 public function get_messenger_channel() { |
|
4463 return $this->messenger_channel; |
|
4464 } |
|
4465 |
|
4466 /** |
|
4467 * Set URL to link the user to when closing the Customizer. |
|
4468 * |
|
4469 * URL is validated. |
|
4470 * |
|
4471 * @since 4.4.0 |
|
4472 * |
|
4473 * @param string $return_url URL for return link. |
|
4474 */ |
|
4475 public function set_return_url( $return_url ) { |
|
4476 $return_url = esc_url_raw( $return_url ); |
|
4477 $return_url = remove_query_arg( wp_removable_query_args(), $return_url ); |
|
4478 $return_url = wp_validate_redirect( $return_url ); |
|
4479 $this->return_url = $return_url; |
|
4480 } |
|
4481 |
|
4482 /** |
|
4483 * Get URL to link the user to when closing the Customizer. |
|
4484 * |
|
4485 * @since 4.4.0 |
|
4486 * |
|
4487 * @return string URL for link to close Customizer. |
|
4488 */ |
|
4489 public function get_return_url() { |
|
4490 $referer = wp_get_referer(); |
|
4491 $excluded_referer_basenames = array( 'customize.php', 'wp-login.php' ); |
|
4492 |
|
4493 if ( $this->return_url ) { |
|
4494 $return_url = $this->return_url; |
|
4495 } else if ( $referer && ! in_array( basename( parse_url( $referer, PHP_URL_PATH ) ), $excluded_referer_basenames, true ) ) { |
|
4496 $return_url = $referer; |
|
4497 } else if ( $this->preview_url ) { |
|
4498 $return_url = $this->preview_url; |
|
4499 } else { |
|
4500 $return_url = home_url( '/' ); |
|
4501 } |
|
4502 return $return_url; |
|
4503 } |
|
4504 |
|
4505 /** |
|
4506 * Set the autofocused constructs. |
|
4507 * |
|
4508 * @since 4.4.0 |
|
4509 * |
|
4510 * @param array $autofocus { |
|
4511 * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused. |
|
4512 * |
|
4513 * @type string [$control] ID for control to be autofocused. |
|
4514 * @type string [$section] ID for section to be autofocused. |
|
4515 * @type string [$panel] ID for panel to be autofocused. |
|
4516 * } |
|
4517 */ |
|
4518 public function set_autofocus( $autofocus ) { |
|
4519 $this->autofocus = array_filter( wp_array_slice_assoc( $autofocus, array( 'panel', 'section', 'control' ) ), 'is_string' ); |
|
4520 } |
|
4521 |
|
4522 /** |
|
4523 * Get the autofocused constructs. |
|
4524 * |
|
4525 * @since 4.4.0 |
|
4526 * |
|
4527 * @return array { |
|
4528 * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused. |
|
4529 * |
|
4530 * @type string [$control] ID for control to be autofocused. |
|
4531 * @type string [$section] ID for section to be autofocused. |
|
4532 * @type string [$panel] ID for panel to be autofocused. |
|
4533 * } |
|
4534 */ |
|
4535 public function get_autofocus() { |
|
4536 return $this->autofocus; |
|
4537 } |
|
4538 |
|
4539 /** |
|
4540 * Get nonces for the Customizer. |
|
4541 * |
|
4542 * @since 4.5.0 |
|
4543 * |
|
4544 * @return array Nonces. |
|
4545 */ |
|
4546 public function get_nonces() { |
|
4547 $nonces = array( |
|
4548 'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ), |
|
4549 'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ), |
|
4550 'switch_themes' => wp_create_nonce( 'switch_themes' ), |
|
4551 'dismiss_autosave_or_lock' => wp_create_nonce( 'customize_dismiss_autosave_or_lock' ), |
|
4552 'override_lock' => wp_create_nonce( 'customize_override_changeset_lock' ), |
|
4553 'trash' => wp_create_nonce( 'trash_customize_changeset' ), |
|
4554 ); |
|
4555 |
|
4556 /** |
|
4557 * Filters nonces for Customizer. |
|
4558 * |
|
4559 * @since 4.2.0 |
|
4560 * |
|
4561 * @param array $nonces Array of refreshed nonces for save and |
|
4562 * preview actions. |
|
4563 * @param WP_Customize_Manager $this WP_Customize_Manager instance. |
|
4564 */ |
|
4565 $nonces = apply_filters( 'customize_refresh_nonces', $nonces, $this ); |
|
4566 |
|
4567 return $nonces; |
|
4568 } |
|
4569 |
|
4570 /** |
|
4571 * Print JavaScript settings for parent window. |
|
4572 * |
|
4573 * @since 4.4.0 |
|
4574 */ |
|
4575 public function customize_pane_settings() { |
|
4576 |
|
4577 $login_url = add_query_arg( array( |
|
4578 'interim-login' => 1, |
|
4579 'customize-login' => 1, |
|
4580 ), wp_login_url() ); |
|
4581 |
|
4582 // Ensure dirty flags are set for modified settings. |
|
4583 foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) { |
|
4584 $setting = $this->get_setting( $setting_id ); |
|
4585 if ( $setting ) { |
|
4586 $setting->dirty = true; |
|
4587 } |
|
4588 } |
|
4589 |
|
4590 $autosave_revision_post = null; |
|
4591 $autosave_autodraft_post = null; |
|
4592 $changeset_post_id = $this->changeset_post_id(); |
|
4593 if ( ! $this->saved_starter_content_changeset && ! $this->autosaved() ) { |
|
4594 if ( $changeset_post_id ) { |
|
4595 if ( is_user_logged_in() ) { |
|
4596 $autosave_revision_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() ); |
|
4597 } |
|
4598 } else { |
|
4599 $autosave_autodraft_posts = $this->get_changeset_posts( array( |
|
4600 'posts_per_page' => 1, |
|
4601 'post_status' => 'auto-draft', |
|
4602 'exclude_restore_dismissed' => true, |
|
4603 ) ); |
|
4604 if ( ! empty( $autosave_autodraft_posts ) ) { |
|
4605 $autosave_autodraft_post = array_shift( $autosave_autodraft_posts ); |
|
4606 } |
|
4607 } |
|
4608 } |
|
4609 |
|
4610 $current_user_can_publish = current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ); |
|
4611 |
|
4612 // @todo Include all of the status labels here from script-loader.php, and then allow it to be filtered. |
|
4613 $status_choices = array(); |
|
4614 if ( $current_user_can_publish ) { |
|
4615 $status_choices[] = array( |
|
4616 'status' => 'publish', |
|
4617 'label' => __( 'Publish' ), |
|
4618 ); |
|
4619 } |
|
4620 $status_choices[] = array( |
|
4621 'status' => 'draft', |
|
4622 'label' => __( 'Save Draft' ), |
|
4623 ); |
|
4624 if ( $current_user_can_publish ) { |
|
4625 $status_choices[] = array( |
|
4626 'status' => 'future', |
|
4627 'label' => _x( 'Schedule', 'customizer changeset action/button label' ), |
|
4628 ); |
|
4629 } |
|
4630 |
|
4631 // Prepare Customizer settings to pass to JavaScript. |
|
4632 $changeset_post = null; |
|
4633 if ( $changeset_post_id ) { |
|
4634 $changeset_post = get_post( $changeset_post_id ); |
|
4635 } |
|
4636 |
|
4637 // Determine initial date to be at present or future, not past. |
|
4638 $current_time = current_time( 'mysql', false ); |
|
4639 $initial_date = $current_time; |
|
4640 if ( $changeset_post ) { |
|
4641 $initial_date = get_the_time( 'Y-m-d H:i:s', $changeset_post->ID ); |
|
4642 if ( $initial_date < $current_time ) { |
|
4643 $initial_date = $current_time; |
|
4644 } |
|
4645 } |
|
4646 |
|
4647 $lock_user_id = false; |
|
4648 if ( $this->changeset_post_id() ) { |
|
4649 $lock_user_id = wp_check_post_lock( $this->changeset_post_id() ); |
|
4650 } |
|
4651 |
|
4652 $settings = array( |
|
4653 'changeset' => array( |
|
4654 'uuid' => $this->changeset_uuid(), |
|
4655 'branching' => $this->branching(), |
|
4656 'autosaved' => $this->autosaved(), |
|
4657 'hasAutosaveRevision' => ! empty( $autosave_revision_post ), |
|
4658 'latestAutoDraftUuid' => $autosave_autodraft_post ? $autosave_autodraft_post->post_name : null, |
|
4659 'status' => $changeset_post ? $changeset_post->post_status : '', |
|
4660 'currentUserCanPublish' => $current_user_can_publish, |
|
4661 'publishDate' => $initial_date, |
|
4662 'statusChoices' => $status_choices, |
|
4663 'lockUser' => $lock_user_id ? $this->get_lock_user_data( $lock_user_id ) : null, |
|
4664 ), |
|
4665 'initialServerDate' => $current_time, |
|
4666 'dateFormat' => get_option( 'date_format' ), |
|
4667 'timeFormat' => get_option( 'time_format' ), |
|
4668 'initialServerTimestamp' => floor( microtime( true ) * 1000 ), |
|
4669 'initialClientTimestamp' => -1, // To be set with JS below. |
|
4670 'timeouts' => array( |
|
4671 'windowRefresh' => 250, |
|
4672 'changesetAutoSave' => AUTOSAVE_INTERVAL * 1000, |
|
4673 'keepAliveCheck' => 2500, |
|
4674 'reflowPaneContents' => 100, |
|
4675 'previewFrameSensitivity' => 2000, |
|
4676 ), |
|
4677 'theme' => array( |
|
4678 'stylesheet' => $this->get_stylesheet(), |
|
4679 'active' => $this->is_theme_active(), |
|
4680 '_canInstall' => current_user_can( 'install_themes' ), |
|
4681 ), |
|
4682 'url' => array( |
|
4683 'preview' => esc_url_raw( $this->get_preview_url() ), |
|
4684 'return' => esc_url_raw( $this->get_return_url() ), |
|
4685 'parent' => esc_url_raw( admin_url() ), |
|
4686 'activated' => esc_url_raw( home_url( '/' ) ), |
|
4687 'ajax' => esc_url_raw( admin_url( 'admin-ajax.php', 'relative' ) ), |
|
4688 'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ), |
|
4689 'isCrossDomain' => $this->is_cross_domain(), |
|
4690 'home' => esc_url_raw( home_url( '/' ) ), |
|
4691 'login' => esc_url_raw( $login_url ), |
|
4692 ), |
|
4693 'browser' => array( |
|
4694 'mobile' => wp_is_mobile(), |
|
4695 'ios' => $this->is_ios(), |
|
4696 ), |
|
4697 'panels' => array(), |
|
4698 'sections' => array(), |
|
4699 'nonce' => $this->get_nonces(), |
|
4700 'autofocus' => $this->get_autofocus(), |
|
4701 'documentTitleTmpl' => $this->get_document_title_template(), |
|
4702 'previewableDevices' => $this->get_previewable_devices(), |
|
4703 'l10n' => array( |
|
4704 'confirmDeleteTheme' => __( 'Are you sure you want to delete this theme?' ), |
|
4705 /* translators: %d: number of theme search results, which cannot currently consider singular vs. plural forms */ |
|
4706 'themeSearchResults' => __( '%d themes found' ), |
|
4707 /* translators: %d: number of themes being displayed, which cannot currently consider singular vs. plural forms */ |
|
4708 'announceThemeCount' => __( 'Displaying %d themes' ), |
|
4709 /* translators: %s: theme name */ |
|
4710 'announceThemeDetails' => __( 'Showing details for theme: %s' ), |
|
4711 ), |
|
4712 ); |
|
4713 |
|
4714 // Temporarily disable installation in Customizer. See #42184. |
|
4715 $filesystem_method = get_filesystem_method(); |
|
4716 ob_start(); |
|
4717 $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() ); |
|
4718 ob_end_clean(); |
|
4719 if ( 'direct' !== $filesystem_method && ! $filesystem_credentials_are_stored ) { |
|
4720 $settings['theme']['_filesystemCredentialsNeeded'] = true; |
|
4721 } |
|
4722 |
|
4723 // Prepare Customize Section objects to pass to JavaScript. |
|
4724 foreach ( $this->sections() as $id => $section ) { |
|
4725 if ( $section->check_capabilities() ) { |
|
4726 $settings['sections'][ $id ] = $section->json(); |
|
4727 } |
|
4728 } |
|
4729 |
|
4730 // Prepare Customize Panel objects to pass to JavaScript. |
|
4731 foreach ( $this->panels() as $panel_id => $panel ) { |
|
4732 if ( $panel->check_capabilities() ) { |
|
4733 $settings['panels'][ $panel_id ] = $panel->json(); |
|
4734 foreach ( $panel->sections as $section_id => $section ) { |
|
4735 if ( $section->check_capabilities() ) { |
|
4736 $settings['sections'][ $section_id ] = $section->json(); |
|
4737 } |
|
4738 } |
|
4739 } |
|
4740 } |
|
4741 |
|
4742 ?> |
|
4743 <script type="text/javascript"> |
|
4744 var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>; |
|
4745 _wpCustomizeSettings.initialClientTimestamp = _.now(); |
|
4746 _wpCustomizeSettings.controls = {}; |
|
4747 _wpCustomizeSettings.settings = {}; |
|
4748 <?php |
|
4749 |
|
4750 // Serialize settings one by one to improve memory usage. |
|
4751 echo "(function ( s ){\n"; |
|
4752 foreach ( $this->settings() as $setting ) { |
|
4753 if ( $setting->check_capabilities() ) { |
|
4754 printf( |
|
4755 "s[%s] = %s;\n", |
|
4756 wp_json_encode( $setting->id ), |
|
4757 wp_json_encode( $setting->json() ) |
|
4758 ); |
|
4759 } |
|
4760 } |
|
4761 echo "})( _wpCustomizeSettings.settings );\n"; |
|
4762 |
|
4763 // Serialize controls one by one to improve memory usage. |
|
4764 echo "(function ( c ){\n"; |
|
4765 foreach ( $this->controls() as $control ) { |
|
4766 if ( $control->check_capabilities() ) { |
|
4767 printf( |
|
4768 "c[%s] = %s;\n", |
|
4769 wp_json_encode( $control->id ), |
|
4770 wp_json_encode( $control->json() ) |
|
4771 ); |
|
4772 } |
|
4773 } |
|
4774 echo "})( _wpCustomizeSettings.controls );\n"; |
|
4775 ?> |
|
4776 </script> |
|
4777 <?php |
|
4778 } |
|
4779 |
|
4780 /** |
|
4781 * Returns a list of devices to allow previewing. |
|
4782 * |
|
4783 * @since 4.5.0 |
|
4784 * |
|
4785 * @return array List of devices with labels and default setting. |
|
4786 */ |
|
4787 public function get_previewable_devices() { |
|
4788 $devices = array( |
|
4789 'desktop' => array( |
|
4790 'label' => __( 'Enter desktop preview mode' ), |
|
4791 'default' => true, |
|
4792 ), |
|
4793 'tablet' => array( |
|
4794 'label' => __( 'Enter tablet preview mode' ), |
|
4795 ), |
|
4796 'mobile' => array( |
|
4797 'label' => __( 'Enter mobile preview mode' ), |
|
4798 ), |
|
4799 ); |
|
4800 |
|
4801 /** |
|
4802 * Filters the available devices to allow previewing in the Customizer. |
|
4803 * |
|
4804 * @since 4.5.0 |
|
4805 * |
|
4806 * @see WP_Customize_Manager::get_previewable_devices() |
|
4807 * |
|
4808 * @param array $devices List of devices with labels and default setting. |
|
4809 */ |
|
4810 $devices = apply_filters( 'customize_previewable_devices', $devices ); |
|
4811 |
|
4812 return $devices; |
1168 } |
4813 } |
1169 |
4814 |
1170 /** |
4815 /** |
1171 * Register some default controls. |
4816 * Register some default controls. |
1172 * |
4817 * |
1173 * @since 3.4.0 |
4818 * @since 3.4.0 |
1174 */ |
4819 */ |
1175 public function register_controls() { |
4820 public function register_controls() { |
1176 |
4821 |
1177 /* Control Types (custom control classes) */ |
4822 /* Themes (controls are loaded via ajax) */ |
1178 $this->register_control_type( 'WP_Customize_Color_Control' ); |
4823 |
1179 $this->register_control_type( 'WP_Customize_Media_Control' ); |
4824 $this->add_panel( new WP_Customize_Themes_Panel( $this, 'themes', array( |
1180 $this->register_control_type( 'WP_Customize_Upload_Control' ); |
4825 'title' => $this->theme()->display( 'Name' ), |
1181 $this->register_control_type( 'WP_Customize_Image_Control' ); |
4826 'description' => ( |
1182 $this->register_control_type( 'WP_Customize_Background_Image_Control' ); |
4827 '<p>' . __( 'Looking for a theme? You can search or browse the WordPress.org theme directory, install and preview themes, then activate them right here.' ) . '</p>' . |
1183 $this->register_control_type( 'WP_Customize_Theme_Control' ); |
4828 '<p>' . __( 'While previewing a new theme, you can continue to tailor things like widgets and menus, and explore theme-specific options.' ) . '</p>' |
1184 |
4829 ), |
1185 /* Themes */ |
4830 'capability' => 'switch_themes', |
1186 |
4831 'priority' => 0, |
1187 $this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array( |
|
1188 'title' => $this->theme()->display( 'Name' ), |
|
1189 'capability' => 'switch_themes', |
|
1190 'priority' => 0, |
|
1191 ) ) ); |
4832 ) ) ); |
|
4833 |
|
4834 $this->add_section( new WP_Customize_Themes_Section( $this, 'installed_themes', array( |
|
4835 'title' => __( 'Installed themes' ), |
|
4836 'action' => 'installed', |
|
4837 'capability' => 'switch_themes', |
|
4838 'panel' => 'themes', |
|
4839 'priority' => 0, |
|
4840 ) ) ); |
|
4841 |
|
4842 if ( ! is_multisite() ) { |
|
4843 $this->add_section( new WP_Customize_Themes_Section( $this, 'wporg_themes', array( |
|
4844 'title' => __( 'WordPress.org themes' ), |
|
4845 'action' => 'wporg', |
|
4846 'filter_type' => 'remote', |
|
4847 'capability' => 'install_themes', |
|
4848 'panel' => 'themes', |
|
4849 'priority' => 5, |
|
4850 ) ) ); |
|
4851 } |
1192 |
4852 |
1193 // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience). |
4853 // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience). |
1194 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array( |
4854 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array( |
1195 'capability' => 'switch_themes', |
4855 'capability' => 'switch_themes', |
1196 ) ) ); |
4856 ) ) ); |
1197 |
4857 |
1198 require_once( ABSPATH . 'wp-admin/includes/theme.php' ); |
4858 /* Site Identity */ |
1199 |
|
1200 // Theme Controls. |
|
1201 |
|
1202 // Add a control for the active/original theme. |
|
1203 if ( ! $this->is_theme_active() ) { |
|
1204 $themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) ); |
|
1205 $active_theme = current( $themes ); |
|
1206 $active_theme['isActiveTheme'] = true; |
|
1207 $this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array( |
|
1208 'theme' => $active_theme, |
|
1209 'section' => 'themes', |
|
1210 'settings' => 'active_theme', |
|
1211 ) ) ); |
|
1212 } |
|
1213 |
|
1214 $themes = wp_prepare_themes_for_js(); |
|
1215 foreach ( $themes as $theme ) { |
|
1216 if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) { |
|
1217 continue; |
|
1218 } |
|
1219 |
|
1220 $theme_id = 'theme_' . $theme['id']; |
|
1221 $theme['isActiveTheme'] = false; |
|
1222 $this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array( |
|
1223 'theme' => $theme, |
|
1224 'section' => 'themes', |
|
1225 'settings' => 'active_theme', |
|
1226 ) ) ); |
|
1227 } |
|
1228 |
|
1229 /* Site Title & Tagline */ |
|
1230 |
4859 |
1231 $this->add_section( 'title_tagline', array( |
4860 $this->add_section( 'title_tagline', array( |
1232 'title' => __( 'Site Title & Tagline' ), |
4861 'title' => __( 'Site Identity' ), |
1233 'priority' => 20, |
4862 'priority' => 20, |
1234 ) ); |
4863 ) ); |
1235 |
4864 |
1236 $this->add_setting( 'blogname', array( |
4865 $this->add_setting( 'blogname', array( |
1237 'default' => get_option( 'blogname' ), |
4866 'default' => get_option( 'blogname' ), |
1329 ) ); |
5108 ) ); |
1330 |
5109 |
1331 $this->add_setting( 'background_image', array( |
5110 $this->add_setting( 'background_image', array( |
1332 'default' => get_theme_support( 'custom-background', 'default-image' ), |
5111 'default' => get_theme_support( 'custom-background', 'default-image' ), |
1333 'theme_supports' => 'custom-background', |
5112 'theme_supports' => 'custom-background', |
|
5113 'sanitize_callback' => array( $this, '_sanitize_background_setting' ), |
1334 ) ); |
5114 ) ); |
1335 |
5115 |
1336 $this->add_setting( new WP_Customize_Background_Image_Setting( $this, 'background_image_thumb', array( |
5116 $this->add_setting( new WP_Customize_Background_Image_Setting( $this, 'background_image_thumb', array( |
1337 'theme_supports' => 'custom-background', |
5117 'theme_supports' => 'custom-background', |
|
5118 'sanitize_callback' => array( $this, '_sanitize_background_setting' ), |
1338 ) ) ); |
5119 ) ) ); |
1339 |
5120 |
1340 $this->add_control( new WP_Customize_Background_Image_Control( $this ) ); |
5121 $this->add_control( new WP_Customize_Background_Image_Control( $this ) ); |
1341 |
5122 |
1342 $this->add_setting( 'background_repeat', array( |
5123 $this->add_setting( 'background_preset', array( |
1343 'default' => get_theme_support( 'custom-background', 'default-repeat' ), |
5124 'default' => get_theme_support( 'custom-background', 'default-preset' ), |
1344 'theme_supports' => 'custom-background', |
5125 'theme_supports' => 'custom-background', |
|
5126 'sanitize_callback' => array( $this, '_sanitize_background_setting' ), |
1345 ) ); |
5127 ) ); |
1346 |
5128 |
1347 $this->add_control( 'background_repeat', array( |
5129 $this->add_control( 'background_preset', array( |
1348 'label' => __( 'Background Repeat' ), |
5130 'label' => _x( 'Preset', 'Background Preset' ), |
1349 'section' => 'background_image', |
5131 'section' => 'background_image', |
1350 'type' => 'radio', |
5132 'type' => 'select', |
1351 'choices' => array( |
5133 'choices' => array( |
1352 'no-repeat' => __('No Repeat'), |
5134 'default' => _x( 'Default', 'Default Preset' ), |
1353 'repeat' => __('Tile'), |
5135 'fill' => __( 'Fill Screen' ), |
1354 'repeat-x' => __('Tile Horizontally'), |
5136 'fit' => __( 'Fit to Screen' ), |
1355 'repeat-y' => __('Tile Vertically'), |
5137 'repeat' => _x( 'Repeat', 'Repeat Image' ), |
|
5138 'custom' => _x( 'Custom', 'Custom Preset' ), |
1356 ), |
5139 ), |
1357 ) ); |
5140 ) ); |
1358 |
5141 |
1359 $this->add_setting( 'background_position_x', array( |
5142 $this->add_setting( 'background_position_x', array( |
1360 'default' => get_theme_support( 'custom-background', 'default-position-x' ), |
5143 'default' => get_theme_support( 'custom-background', 'default-position-x' ), |
1361 'theme_supports' => 'custom-background', |
5144 'theme_supports' => 'custom-background', |
|
5145 'sanitize_callback' => array( $this, '_sanitize_background_setting' ), |
1362 ) ); |
5146 ) ); |
1363 |
5147 |
1364 $this->add_control( 'background_position_x', array( |
5148 $this->add_setting( 'background_position_y', array( |
1365 'label' => __( 'Background Position' ), |
5149 'default' => get_theme_support( 'custom-background', 'default-position-y' ), |
|
5150 'theme_supports' => 'custom-background', |
|
5151 'sanitize_callback' => array( $this, '_sanitize_background_setting' ), |
|
5152 ) ); |
|
5153 |
|
5154 $this->add_control( new WP_Customize_Background_Position_Control( $this, 'background_position', array( |
|
5155 'label' => __( 'Image Position' ), |
|
5156 'section' => 'background_image', |
|
5157 'settings' => array( |
|
5158 'x' => 'background_position_x', |
|
5159 'y' => 'background_position_y', |
|
5160 ), |
|
5161 ) ) ); |
|
5162 |
|
5163 $this->add_setting( 'background_size', array( |
|
5164 'default' => get_theme_support( 'custom-background', 'default-size' ), |
|
5165 'theme_supports' => 'custom-background', |
|
5166 'sanitize_callback' => array( $this, '_sanitize_background_setting' ), |
|
5167 ) ); |
|
5168 |
|
5169 $this->add_control( 'background_size', array( |
|
5170 'label' => __( 'Image Size' ), |
1366 'section' => 'background_image', |
5171 'section' => 'background_image', |
1367 'type' => 'radio', |
5172 'type' => 'select', |
1368 'choices' => array( |
5173 'choices' => array( |
1369 'left' => __('Left'), |
5174 'auto' => __( 'Original' ), |
1370 'center' => __('Center'), |
5175 'contain' => __( 'Fit to Screen' ), |
1371 'right' => __('Right'), |
5176 'cover' => __( 'Fill Screen' ), |
1372 ), |
5177 ), |
1373 ) ); |
5178 ) ); |
1374 |
5179 |
|
5180 $this->add_setting( 'background_repeat', array( |
|
5181 'default' => get_theme_support( 'custom-background', 'default-repeat' ), |
|
5182 'sanitize_callback' => array( $this, '_sanitize_background_setting' ), |
|
5183 'theme_supports' => 'custom-background', |
|
5184 ) ); |
|
5185 |
|
5186 $this->add_control( 'background_repeat', array( |
|
5187 'label' => __( 'Repeat Background Image' ), |
|
5188 'section' => 'background_image', |
|
5189 'type' => 'checkbox', |
|
5190 ) ); |
|
5191 |
1375 $this->add_setting( 'background_attachment', array( |
5192 $this->add_setting( 'background_attachment', array( |
1376 'default' => get_theme_support( 'custom-background', 'default-attachment' ), |
5193 'default' => get_theme_support( 'custom-background', 'default-attachment' ), |
1377 'theme_supports' => 'custom-background', |
5194 'sanitize_callback' => array( $this, '_sanitize_background_setting' ), |
|
5195 'theme_supports' => 'custom-background', |
1378 ) ); |
5196 ) ); |
1379 |
5197 |
1380 $this->add_control( 'background_attachment', array( |
5198 $this->add_control( 'background_attachment', array( |
1381 'label' => __( 'Background Attachment' ), |
5199 'label' => __( 'Scroll with Page' ), |
1382 'section' => 'background_image', |
5200 'section' => 'background_image', |
1383 'type' => 'radio', |
5201 'type' => 'checkbox', |
1384 'choices' => array( |
|
1385 'scroll' => __('Scroll'), |
|
1386 'fixed' => __('Fixed'), |
|
1387 ), |
|
1388 ) ); |
5202 ) ); |
|
5203 |
1389 |
5204 |
1390 // If the theme is using the default background callback, we can update |
5205 // If the theme is using the default background callback, we can update |
1391 // the background CSS using postMessage. |
5206 // the background CSS using postMessage. |
1392 if ( get_theme_support( 'custom-background', 'wp-head-callback' ) === '_custom_background_cb' ) { |
5207 if ( get_theme_support( 'custom-background', 'wp-head-callback' ) === '_custom_background_cb' ) { |
1393 foreach ( array( 'color', 'image', 'position_x', 'repeat', 'attachment' ) as $prop ) { |
5208 foreach ( array( 'color', 'image', 'preset', 'position_x', 'position_y', 'size', 'repeat', 'attachment' ) as $prop ) { |
1394 $this->get_setting( 'background_' . $prop )->transport = 'postMessage'; |
5209 $this->get_setting( 'background_' . $prop )->transport = 'postMessage'; |
1395 } |
5210 } |
1396 } |
5211 } |
1397 |
5212 |
1398 /* Nav Menus */ |
5213 /* |
1399 |
5214 * Static Front Page |
1400 $locations = get_registered_nav_menus(); |
5215 * See also https://core.trac.wordpress.org/ticket/19627 which introduces the static-front-page theme_support. |
1401 $menus = wp_get_nav_menus(); |
5216 * The following replicates behavior from options-reading.php. |
1402 $num_locations = count( array_keys( $locations ) ); |
5217 */ |
1403 |
5218 |
1404 if ( 1 == $num_locations ) { |
5219 $this->add_section( 'static_front_page', array( |
1405 $description = __( 'Your theme supports one menu. Select which menu you would like to use.' ); |
5220 'title' => __( 'Homepage Settings' ), |
|
5221 'priority' => 120, |
|
5222 'description' => __( 'You can choose what’s displayed on the homepage of your site. It can be posts in reverse chronological order (classic blog), or a fixed/static page. To set a static homepage, you first need to create two Pages. One will become the homepage, and the other will be where your posts are displayed.' ), |
|
5223 'active_callback' => array( $this, 'has_published_pages' ), |
|
5224 ) ); |
|
5225 |
|
5226 $this->add_setting( 'show_on_front', array( |
|
5227 'default' => get_option( 'show_on_front' ), |
|
5228 'capability' => 'manage_options', |
|
5229 'type' => 'option', |
|
5230 ) ); |
|
5231 |
|
5232 $this->add_control( 'show_on_front', array( |
|
5233 'label' => __( 'Your homepage displays' ), |
|
5234 'section' => 'static_front_page', |
|
5235 'type' => 'radio', |
|
5236 'choices' => array( |
|
5237 'posts' => __( 'Your latest posts' ), |
|
5238 'page' => __( 'A static page' ), |
|
5239 ), |
|
5240 ) ); |
|
5241 |
|
5242 $this->add_setting( 'page_on_front', array( |
|
5243 'type' => 'option', |
|
5244 'capability' => 'manage_options', |
|
5245 ) ); |
|
5246 |
|
5247 $this->add_control( 'page_on_front', array( |
|
5248 'label' => __( 'Homepage' ), |
|
5249 'section' => 'static_front_page', |
|
5250 'type' => 'dropdown-pages', |
|
5251 'allow_addition' => true, |
|
5252 ) ); |
|
5253 |
|
5254 $this->add_setting( 'page_for_posts', array( |
|
5255 'type' => 'option', |
|
5256 'capability' => 'manage_options', |
|
5257 ) ); |
|
5258 |
|
5259 $this->add_control( 'page_for_posts', array( |
|
5260 'label' => __( 'Posts page' ), |
|
5261 'section' => 'static_front_page', |
|
5262 'type' => 'dropdown-pages', |
|
5263 'allow_addition' => true, |
|
5264 ) ); |
|
5265 |
|
5266 /* Custom CSS */ |
|
5267 $section_description = '<p>'; |
|
5268 $section_description .= __( 'Add your own CSS code here to customize the appearance and layout of your site.' ); |
|
5269 $section_description .= sprintf( |
|
5270 ' <a href="%1$s" class="external-link" target="_blank">%2$s<span class="screen-reader-text"> %3$s</span></a>', |
|
5271 esc_url( __( 'https://codex.wordpress.org/CSS' ) ), |
|
5272 __( 'Learn more about CSS' ), |
|
5273 /* translators: accessibility text */ |
|
5274 __( '(opens in a new window)' ) |
|
5275 ); |
|
5276 $section_description .= '</p>'; |
|
5277 |
|
5278 $section_description .= '<p id="editor-keyboard-trap-help-1">' . __( 'When using a keyboard to navigate:' ) . '</p>'; |
|
5279 $section_description .= '<ul>'; |
|
5280 $section_description .= '<li id="editor-keyboard-trap-help-2">' . __( 'In the editing area, the Tab key enters a tab character.' ) . '</li>'; |
|
5281 $section_description .= '<li id="editor-keyboard-trap-help-3">' . __( 'To move away from this area, press the Esc key followed by the Tab key.' ) . '</li>'; |
|
5282 $section_description .= '<li id="editor-keyboard-trap-help-4">' . __( 'Screen reader users: when in forms mode, you may need to press the escape key twice.' ) . '</li>'; |
|
5283 $section_description .= '</ul>'; |
|
5284 |
|
5285 if ( 'false' !== wp_get_current_user()->syntax_highlighting ) { |
|
5286 $section_description .= '<p>'; |
|
5287 $section_description .= sprintf( |
|
5288 /* translators: 1: link to user profile, 2: additional link attributes, 3: accessibility text */ |
|
5289 __( 'The edit field automatically highlights code syntax. You can disable this in your <a href="%1$s" %2$s>user profile%3$s</a> to work in plain text mode.' ), |
|
5290 esc_url( get_edit_profile_url() ), |
|
5291 'class="external-link" target="_blank"', |
|
5292 sprintf( '<span class="screen-reader-text"> %s</span>', |
|
5293 /* translators: accessibility text */ |
|
5294 __( '(opens in a new window)' ) |
|
5295 ) |
|
5296 ); |
|
5297 $section_description .= '</p>'; |
|
5298 } |
|
5299 |
|
5300 $section_description .= '<p class="section-description-buttons">'; |
|
5301 $section_description .= '<button type="button" class="button-link section-description-close">' . __( 'Close' ) . '</button>'; |
|
5302 $section_description .= '</p>'; |
|
5303 |
|
5304 $this->add_section( 'custom_css', array( |
|
5305 'title' => __( 'Additional CSS' ), |
|
5306 'priority' => 200, |
|
5307 'description_hidden' => true, |
|
5308 'description' => $section_description, |
|
5309 ) ); |
|
5310 |
|
5311 $custom_css_setting = new WP_Customize_Custom_CSS_Setting( $this, sprintf( 'custom_css[%s]', get_stylesheet() ), array( |
|
5312 'capability' => 'edit_css', |
|
5313 'default' => '', |
|
5314 ) ); |
|
5315 $this->add_setting( $custom_css_setting ); |
|
5316 |
|
5317 $this->add_control( new WP_Customize_Code_Editor_Control( $this, 'custom_css', array( |
|
5318 'label' => __( 'CSS code' ), |
|
5319 'section' => 'custom_css', |
|
5320 'settings' => array( 'default' => $custom_css_setting->id ), |
|
5321 'code_type' => 'text/css', |
|
5322 'input_attrs' => array( |
|
5323 'aria-describedby' => 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4', |
|
5324 ), |
|
5325 ) ) ); |
|
5326 } |
|
5327 |
|
5328 /** |
|
5329 * Return whether there are published pages. |
|
5330 * |
|
5331 * Used as active callback for static front page section and controls. |
|
5332 * |
|
5333 * @since 4.7.0 |
|
5334 * |
|
5335 * @returns bool Whether there are published (or to be published) pages. |
|
5336 */ |
|
5337 public function has_published_pages() { |
|
5338 |
|
5339 $setting = $this->get_setting( 'nav_menus_created_posts' ); |
|
5340 if ( $setting ) { |
|
5341 foreach ( $setting->value() as $post_id ) { |
|
5342 if ( 'page' === get_post_type( $post_id ) ) { |
|
5343 return true; |
|
5344 } |
|
5345 } |
|
5346 } |
|
5347 return 0 !== count( get_pages() ); |
|
5348 } |
|
5349 |
|
5350 /** |
|
5351 * Add settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets |
|
5352 * |
|
5353 * @since 4.2.0 |
|
5354 * |
|
5355 * @see add_dynamic_settings() |
|
5356 */ |
|
5357 public function register_dynamic_settings() { |
|
5358 $setting_ids = array_keys( $this->unsanitized_post_values() ); |
|
5359 $this->add_dynamic_settings( $setting_ids ); |
|
5360 } |
|
5361 |
|
5362 /** |
|
5363 * Load themes into the theme browsing/installation UI. |
|
5364 * |
|
5365 * @since 4.9.0 |
|
5366 */ |
|
5367 public function handle_load_themes_request() { |
|
5368 check_ajax_referer( 'switch_themes', 'nonce' ); |
|
5369 |
|
5370 if ( ! current_user_can( 'switch_themes' ) ) { |
|
5371 wp_die( -1 ); |
|
5372 } |
|
5373 |
|
5374 if ( empty( $_POST['theme_action'] ) ) { |
|
5375 wp_send_json_error( 'missing_theme_action' ); |
|
5376 } |
|
5377 $theme_action = sanitize_key( $_POST['theme_action'] ); |
|
5378 $themes = array(); |
|
5379 $args = array(); |
|
5380 |
|
5381 // Define query filters based on user input. |
|
5382 if ( ! array_key_exists( 'search', $_POST ) ) { |
|
5383 $args['search'] = ''; |
1406 } else { |
5384 } else { |
1407 $description = sprintf( _n( 'Your theme supports %s menu. Select which menu appears in each location.', 'Your theme supports %s menus. Select which menu appears in each location.', $num_locations ), number_format_i18n( $num_locations ) ); |
5385 $args['search'] = sanitize_text_field( wp_unslash( $_POST['search'] ) ); |
1408 } |
5386 } |
1409 |
5387 |
1410 $this->add_section( 'nav', array( |
5388 if ( ! array_key_exists( 'tags', $_POST ) ) { |
1411 'title' => __( 'Navigation' ), |
5389 $args['tag'] = ''; |
1412 'theme_supports' => 'menus', |
5390 } else { |
1413 'priority' => 100, |
5391 $args['tag'] = array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['tags'] ) ); |
1414 'description' => $description . "\n\n" . __( 'You can edit your menu content on the Menus screen in the Appearance section.' ), |
5392 } |
1415 ) ); |
5393 |
1416 |
5394 if ( ! array_key_exists( 'page', $_POST ) ) { |
1417 if ( $menus ) { |
5395 $args['page'] = 1; |
1418 $choices = array( '' => __( '— Select —' ) ); |
5396 } else { |
1419 foreach ( $menus as $menu ) { |
5397 $args['page'] = absint( $_POST['page'] ); |
1420 $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '…' ); |
5398 } |
1421 } |
5399 |
1422 |
5400 require_once ABSPATH . 'wp-admin/includes/theme.php'; |
1423 foreach ( $locations as $location => $description ) { |
5401 |
1424 $menu_setting_id = "nav_menu_locations[{$location}]"; |
5402 if ( 'installed' === $theme_action ) { |
1425 |
5403 |
1426 $this->add_setting( $menu_setting_id, array( |
5404 // Load all installed themes from wp_prepare_themes_for_js(). |
1427 'sanitize_callback' => 'absint', |
5405 $themes = array( 'themes' => wp_prepare_themes_for_js() ); |
1428 'theme_supports' => 'menus', |
5406 foreach ( $themes['themes'] as &$theme ) { |
|
5407 $theme['type'] = 'installed'; |
|
5408 $theme['active'] = ( isset( $_POST['customized_theme'] ) && $_POST['customized_theme'] === $theme['id'] ); |
|
5409 } |
|
5410 |
|
5411 } elseif ( 'wporg' === $theme_action ) { |
|
5412 |
|
5413 // Load WordPress.org themes from the .org API and normalize data to match installed theme objects. |
|
5414 if ( ! current_user_can( 'install_themes' ) ) { |
|
5415 wp_die( -1 ); |
|
5416 } |
|
5417 |
|
5418 // Arguments for all queries. |
|
5419 $wporg_args = array( |
|
5420 'per_page' => 100, |
|
5421 'fields' => array( |
|
5422 'screenshot_url' => true, |
|
5423 'description' => true, |
|
5424 'rating' => true, |
|
5425 'downloaded' => true, |
|
5426 'downloadlink' => true, |
|
5427 'last_updated' => true, |
|
5428 'homepage' => true, |
|
5429 'num_ratings' => true, |
|
5430 'tags' => true, |
|
5431 'parent' => true, |
|
5432 // 'extended_author' => true, @todo: WordPress.org throws a 500 server error when this is here. |
|
5433 ), |
|
5434 ); |
|
5435 |
|
5436 $args = array_merge( $wporg_args, $args ); |
|
5437 |
|
5438 if ( '' === $args['search'] && '' === $args['tag'] ) { |
|
5439 $args['browse'] = 'new'; // Sort by latest themes by default. |
|
5440 } |
|
5441 |
|
5442 // Load themes from the .org API. |
|
5443 $themes = themes_api( 'query_themes', $args ); |
|
5444 if ( is_wp_error( $themes ) ) { |
|
5445 wp_send_json_error(); |
|
5446 } |
|
5447 |
|
5448 // This list matches the allowed tags in wp-admin/includes/theme-install.php. |
|
5449 $themes_allowedtags = array_fill_keys( |
|
5450 array( 'a', 'abbr', 'acronym', 'code', 'pre', 'em', 'strong', 'div', 'p', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img' ), |
|
5451 array() |
|
5452 ); |
|
5453 $themes_allowedtags['a'] = array_fill_keys( array( 'href', 'title', 'target' ), true ); |
|
5454 $themes_allowedtags['acronym']['title'] = true; |
|
5455 $themes_allowedtags['abbr']['title'] = true; |
|
5456 $themes_allowedtags['img'] = array_fill_keys( array( 'src', 'class', 'alt' ), true ); |
|
5457 |
|
5458 // Prepare a list of installed themes to check against before the loop. |
|
5459 $installed_themes = array(); |
|
5460 $wp_themes = wp_get_themes(); |
|
5461 foreach ( $wp_themes as $theme ) { |
|
5462 $installed_themes[] = $theme->get_stylesheet(); |
|
5463 } |
|
5464 $update_php = network_admin_url( 'update.php?action=install-theme' ); |
|
5465 |
|
5466 // Set up properties for themes available on WordPress.org. |
|
5467 foreach ( $themes->themes as &$theme ) { |
|
5468 $theme->install_url = add_query_arg( array( |
|
5469 'theme' => $theme->slug, |
|
5470 '_wpnonce' => wp_create_nonce( 'install-theme_' . $theme->slug ), |
|
5471 ), $update_php ); |
|
5472 |
|
5473 $theme->name = wp_kses( $theme->name, $themes_allowedtags ); |
|
5474 $theme->author = wp_kses( $theme->author, $themes_allowedtags ); |
|
5475 $theme->version = wp_kses( $theme->version, $themes_allowedtags ); |
|
5476 $theme->description = wp_kses( $theme->description, $themes_allowedtags ); |
|
5477 $theme->tags = implode( ', ', $theme->tags ); |
|
5478 $theme->stars = wp_star_rating( array( |
|
5479 'rating' => $theme->rating, |
|
5480 'type' => 'percent', |
|
5481 'number' => $theme->num_ratings, |
|
5482 'echo' => false, |
1429 ) ); |
5483 ) ); |
1430 |
5484 $theme->num_ratings = number_format_i18n( $theme->num_ratings ); |
1431 $this->add_control( $menu_setting_id, array( |
5485 $theme->preview_url = set_url_scheme( $theme->preview_url ); |
1432 'label' => $description, |
5486 |
1433 'section' => 'nav', |
5487 // Handle themes that are already installed as installed themes. |
1434 'type' => 'select', |
5488 if ( in_array( $theme->slug, $installed_themes, true ) ) { |
1435 'choices' => $choices, |
5489 $theme->type = 'installed'; |
1436 ) ); |
5490 } else { |
1437 } |
5491 $theme->type = $theme_action; |
1438 } |
5492 } |
1439 |
5493 |
1440 /* Static Front Page */ |
5494 // Set active based on customized theme. |
1441 // #WP19627 |
5495 $theme->active = ( isset( $_POST['customized_theme'] ) && $_POST['customized_theme'] === $theme->slug ); |
1442 |
5496 |
1443 // Replicate behavior from options-reading.php and hide front page options if there are no pages |
5497 // Map available theme properties to installed theme properties. |
1444 if ( get_pages() ) { |
5498 $theme->id = $theme->slug; |
1445 $this->add_section( 'static_front_page', array( |
5499 $theme->screenshot = array( $theme->screenshot_url ); |
1446 'title' => __( 'Static Front Page' ), |
5500 $theme->authorAndUri = $theme->author; |
1447 // 'theme_supports' => 'static-front-page', |
5501 // The .org API can return the full parent theme details if passed the 'parent' arg, or if passed the 'template' option it'll return that in the event it's a child theme. |
1448 'priority' => 120, |
5502 if ( isset( $theme->parent ) ) { |
1449 'description' => __( 'Your theme supports a static front page.' ), |
5503 $theme->parent = $theme->parent['slug']; |
1450 ) ); |
5504 } else { |
1451 |
5505 $theme->parent = false; |
1452 $this->add_setting( 'show_on_front', array( |
5506 } |
1453 'default' => get_option( 'show_on_front' ), |
5507 unset( $theme->slug ); |
1454 'capability' => 'manage_options', |
5508 unset( $theme->screenshot_url ); |
1455 'type' => 'option', |
5509 unset( $theme->author ); |
1456 // 'theme_supports' => 'static-front-page', |
5510 } // End foreach(). |
1457 ) ); |
5511 } // End if(). |
1458 |
5512 |
1459 $this->add_control( 'show_on_front', array( |
5513 /** |
1460 'label' => __( 'Front page displays' ), |
5514 * Filters the theme data loaded in the customizer. |
1461 'section' => 'static_front_page', |
5515 * |
1462 'type' => 'radio', |
5516 * This allows theme data to be loading from an external source, |
1463 'choices' => array( |
5517 * or modification of data loaded from `wp_prepare_themes_for_js()` |
1464 'posts' => __( 'Your latest posts' ), |
5518 * or WordPress.org via `themes_api()`. |
1465 'page' => __( 'A static page' ), |
5519 * |
1466 ), |
5520 * @since 4.9.0 |
1467 ) ); |
5521 * |
1468 |
5522 * @see wp_prepare_themes_for_js() |
1469 $this->add_setting( 'page_on_front', array( |
5523 * @see themes_api() |
1470 'type' => 'option', |
5524 * @see WP_Customize_Manager::__construct() |
1471 'capability' => 'manage_options', |
5525 * |
1472 // 'theme_supports' => 'static-front-page', |
5526 * @param array $themes Nested array of theme data. |
1473 ) ); |
5527 * @param array $args List of arguments, such as page, search term, and tags to query for. |
1474 |
5528 * @param WP_Customize_Manager $manager Instance of Customize manager. |
1475 $this->add_control( 'page_on_front', array( |
5529 */ |
1476 'label' => __( 'Front page' ), |
5530 $themes = apply_filters( 'customize_load_themes', $themes, $args, $this ); |
1477 'section' => 'static_front_page', |
5531 |
1478 'type' => 'dropdown-pages', |
5532 wp_send_json_success( $themes ); |
1479 ) ); |
5533 } |
1480 |
5534 |
1481 $this->add_setting( 'page_for_posts', array( |
|
1482 'type' => 'option', |
|
1483 'capability' => 'manage_options', |
|
1484 // 'theme_supports' => 'static-front-page', |
|
1485 ) ); |
|
1486 |
|
1487 $this->add_control( 'page_for_posts', array( |
|
1488 'label' => __( 'Posts page' ), |
|
1489 'section' => 'static_front_page', |
|
1490 'type' => 'dropdown-pages', |
|
1491 ) ); |
|
1492 } |
|
1493 } |
|
1494 |
|
1495 /** |
|
1496 * Add settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets |
|
1497 * |
|
1498 * @since 4.2.0 |
|
1499 * @access public |
|
1500 * |
|
1501 * @see add_dynamic_settings() |
|
1502 */ |
|
1503 public function register_dynamic_settings() { |
|
1504 $this->add_dynamic_settings( array_keys( $this->unsanitized_post_values() ) ); |
|
1505 } |
|
1506 |
5535 |
1507 /** |
5536 /** |
1508 * Callback for validating the header_textcolor value. |
5537 * Callback for validating the header_textcolor value. |
1509 * |
5538 * |
1510 * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash(). |
5539 * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash(). |
1511 * Returns default text color if hex color is empty. |
5540 * Returns default text color if hex color is empty. |
1512 * |
5541 * |
1513 * @since 3.4.0 |
5542 * @since 3.4.0 |
1514 * |
5543 * |
1515 * @param string $color |
5544 * @param string $color |
1516 * @return string |
5545 * @return mixed |
1517 */ |
5546 */ |
1518 public function _sanitize_header_textcolor( $color ) { |
5547 public function _sanitize_header_textcolor( $color ) { |
1519 if ( 'blank' === $color ) |
5548 if ( 'blank' === $color ) |
1520 return 'blank'; |
5549 return 'blank'; |
1521 |
5550 |