WP_Customize_Manager   F
last analyzed

Complexity

Total Complexity 525

Size/Duplication

Total Lines 4417
Duplicated Lines 4.23 %

Coupling/Cohesion

Components 2
Dependencies 24

Importance

Changes 0
Metric Value
dl 187
loc 4417
rs 0.5665
c 0
b 0
f 0
wmc 525
lcom 2
cbo 24

96 Methods

Rating   Name   Duplication   Size   Complexity  
D __construct() 8 127 9
A doing_ajax() 0 15 4
B wp_die() 0 34 4
A wp_die_handler() 0 9 3
F setup_theme() 15 89 20
A after_setup_theme() 0 7 4
B start_previewing_theme() 31 31 3
B stop_previewing_theme() 30 30 3
A changeset_uuid() 0 3 1
A theme() 0 6 2
A settings() 0 3 1
A controls() 0 3 1
A containers() 0 3 1
A sections() 0 3 1
A panels() 0 3 1
A is_theme_active() 0 3 1
B wp_loaded() 0 29 5
A wp_redirect_status() 0 9 3
B find_changeset_post_id() 0 27 4
A changeset_post_id() 0 13 4
B get_changeset_post_data() 0 20 7
A changeset_data() 0 17 4
F import_theme_starter_content() 48 382 99
C prepare_starter_content_attachments() 0 51 12
A _save_starter_content_changeset() 0 13 2
C unsanitized_post_values() 0 55 14
B post_value() 0 16 5
B set_post_value() 0 34 1
B customize_preview_init() 4 48 4
A filter_iframe_security_headers() 0 6 1
C add_state_query_params() 0 32 8
A customize_preview_override_404_status() 0 3 1
A customize_preview_base() 0 3 1
A customize_preview_html5() 0 3 1
A customize_preview_loading_style() 0 21 1
B remove_frameless_preview_messenger_channel() 0 28 2
F customize_preview_settings() 19 112 17
A customize_preview_signature() 0 3 1
A remove_preview_signature() 0 5 1
A is_preview() 0 3 1
A get_template() 0 3 1
A get_stylesheet() 0 3 1
A get_template_root() 0 3 1
A get_stylesheet_root() 0 3 1
A current_theme() 0 3 1
A prepare_setting_validity_for_js() 0 14 3
F save() 0 134 27
F save_changeset_post() 0 321 57
A _filter_revision_post_has_changed() 0 7 2
F _publish_changeset_values() 0 140 20
B update_stashed_theme_mod_settings() 0 28 5
A refresh_nonces() 0 7 2
A add_setting() 0 18 2
B add_dynamic_settings() 0 46 4
A get_setting() 0 5 2
A remove_setting() 0 3 1
A add_panel() 0 10 2
A get_panel() 0 5 2
A remove_panel() 0 13 2
A register_panel_type() 0 3 1
A render_panel_templates() 0 6 2
A add_section() 0 10 2
A get_section() 0 4 2
A remove_section() 0 3 1
A register_section_type() 0 3 1
A render_section_templates() 0 6 2
A add_control() 0 10 2
A get_control() 0 4 2
A remove_control() 0 3 1
A register_control_type() 0 3 1
A render_control_templates() 0 17 2
A _cmp_priority() 0 9 2
C prepare_controls() 0 75 10
A enqueue_control_scripts() 0 5 2
A is_ios() 0 3 2
A get_document_title_template() 0 11 2
A set_preview_url() 0 4 1
A get_preview_url() 0 8 2
A is_cross_domain() 0 6 1
A get_allowed_urls() 0 18 3
A get_messenger_channel() 0 3 1
A set_return_url() 0 6 1
B get_return_url() 0 15 5
A set_autofocus() 0 3 1
A get_autofocus() 0 3 1
F customize_pane_settings() 19 109 14
D register_controls() 0 508 13
A has_published_pages() 0 12 4
A register_dynamic_settings() 0 4 1
A _sanitize_header_textcolor() 0 10 3
C _sanitize_background_setting() 13 32 16
A export_header_video_settings() 0 7 2
B _validate_header_video() 0 20 5
A _validate_external_header_video() 0 9 3
A _sanitize_external_header_video() 0 3 1
A _render_custom_logo_partial() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WP_Customize_Manager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WP_Customize_Manager, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * WordPress Customize Manager classes
4
 *
5
 * @package WordPress
6
 * @subpackage Customize
7
 * @since 3.4.0
8
 */
9
10
/**
11
 * Customize Manager class.
12
 *
13
 * Bootstraps the Customize experience on the server-side.
14
 *
15
 * Sets up the theme-switching process if a theme other than the active one is
16
 * being previewed and customized.
17
 *
18
 * Serves as a factory for Customize Controls and Settings, and
19
 * instantiates default Customize Controls and Settings.
20
 *
21
 * @since 3.4.0
22
 */
23
final class WP_Customize_Manager {
24
	/**
25
	 * An instance of the theme being previewed.
26
	 *
27
	 * @since 3.4.0
28
	 * @access protected
29
	 * @var WP_Theme
30
	 */
31
	protected $theme;
32
33
	/**
34
	 * The directory name of the previously active theme (within the theme_root).
35
	 *
36
	 * @since 3.4.0
37
	 * @access protected
38
	 * @var string
39
	 */
40
	protected $original_stylesheet;
41
42
	/**
43
	 * Whether this is a Customizer pageload.
44
	 *
45
	 * @since 3.4.0
46
	 * @access protected
47
	 * @var bool
48
	 */
49
	protected $previewing = false;
50
51
	/**
52
	 * Methods and properties dealing with managing widgets in the Customizer.
53
	 *
54
	 * @since 3.9.0
55
	 * @access public
56
	 * @var WP_Customize_Widgets
57
	 */
58
	public $widgets;
59
60
	/**
61
	 * Methods and properties dealing with managing nav menus in the Customizer.
62
	 *
63
	 * @since 4.3.0
64
	 * @access public
65
	 * @var WP_Customize_Nav_Menus
66
	 */
67
	public $nav_menus;
68
69
	/**
70
	 * Methods and properties dealing with selective refresh in the Customizer preview.
71
	 *
72
	 * @since 4.5.0
73
	 * @access public
74
	 * @var WP_Customize_Selective_Refresh
75
	 */
76
	public $selective_refresh;
77
78
	/**
79
	 * Registered instances of WP_Customize_Setting.
80
	 *
81
	 * @since 3.4.0
82
	 * @access protected
83
	 * @var array
84
	 */
85
	protected $settings = array();
86
87
	/**
88
	 * Sorted top-level instances of WP_Customize_Panel and WP_Customize_Section.
89
	 *
90
	 * @since 4.0.0
91
	 * @access protected
92
	 * @var array
93
	 */
94
	protected $containers = array();
95
96
	/**
97
	 * Registered instances of WP_Customize_Panel.
98
	 *
99
	 * @since 4.0.0
100
	 * @access protected
101
	 * @var array
102
	 */
103
	protected $panels = array();
104
105
	/**
106
	 * List of core components.
107
	 *
108
	 * @since 4.5.0
109
	 * @access protected
110
	 * @var array
111
	 */
112
	protected $components = array( 'widgets', 'nav_menus' );
113
114
	/**
115
	 * Registered instances of WP_Customize_Section.
116
	 *
117
	 * @since 3.4.0
118
	 * @access protected
119
	 * @var array
120
	 */
121
	protected $sections = array();
122
123
	/**
124
	 * Registered instances of WP_Customize_Control.
125
	 *
126
	 * @since 3.4.0
127
	 * @access protected
128
	 * @var array
129
	 */
130
	protected $controls = array();
131
132
	/**
133
	 * Panel types that may be rendered from JS templates.
134
	 *
135
	 * @since 4.3.0
136
	 * @access protected
137
	 * @var array
138
	 */
139
	protected $registered_panel_types = array();
140
141
	/**
142
	 * Section types that may be rendered from JS templates.
143
	 *
144
	 * @since 4.3.0
145
	 * @access protected
146
	 * @var array
147
	 */
148
	protected $registered_section_types = array();
149
150
	/**
151
	 * Control types that may be rendered from JS templates.
152
	 *
153
	 * @since 4.1.0
154
	 * @access protected
155
	 * @var array
156
	 */
157
	protected $registered_control_types = array();
158
159
	/**
160
	 * Initial URL being previewed.
161
	 *
162
	 * @since 4.4.0
163
	 * @access protected
164
	 * @var string
165
	 */
166
	protected $preview_url;
167
168
	/**
169
	 * URL to link the user to when closing the Customizer.
170
	 *
171
	 * @since 4.4.0
172
	 * @access protected
173
	 * @var string
174
	 */
175
	protected $return_url;
176
177
	/**
178
	 * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
179
	 *
180
	 * @since 4.4.0
181
	 * @access protected
182
	 * @var array
183
	 */
184
	protected $autofocus = array();
185
186
	/**
187
	 * Messenger channel.
188
	 *
189
	 * @since 4.7.0
190
	 * @access protected
191
	 * @var string
192
	 */
193
	protected $messenger_channel;
194
195
	/**
196
	 * Unsanitized values for Customize Settings parsed from $_POST['customized'].
197
	 *
198
	 * @var array
199
	 */
200
	private $_post_values;
201
202
	/**
203
	 * Changeset UUID.
204
	 *
205
	 * @since 4.7.0
206
	 * @access private
207
	 * @var string
208
	 */
209
	private $_changeset_uuid;
210
211
	/**
212
	 * Changeset post ID.
213
	 *
214
	 * @since 4.7.0
215
	 * @access private
216
	 * @var int|false
217
	 */
218
	private $_changeset_post_id;
219
220
	/**
221
	 * Changeset data loaded from a customize_changeset post.
222
	 *
223
	 * @since 4.7.0
224
	 * @access private
225
	 * @var array
226
	 */
227
	private $_changeset_data;
228
229
	/**
230
	 * Constructor.
231
	 *
232
	 * @since 3.4.0
233
	 * @since 4.7.0 Added $args param.
234
	 *
235
	 * @param array $args {
236
	 *     Args.
237
	 *
238
	 *     @type string $changeset_uuid    Changeset UUID, the post_name for the customize_changeset post containing the customized state. Defaults to new UUID.
239
	 *     @type string $theme             Theme to be previewed (for theme switch). Defaults to customize_theme or theme query params.
240
	 *     @type string $messenger_channel Messenger channel. Defaults to customize_messenger_channel query param.
241
	 * }
242
	 */
243
	public function __construct( $args = array() ) {
244
245
		$args = array_merge(
246
			array_fill_keys( array( 'changeset_uuid', 'theme', 'messenger_channel' ), null ),
247
			$args
248
		);
249
250
		// Note that the UUID format will be validated in the setup_theme() method.
251
		if ( ! isset( $args['changeset_uuid'] ) ) {
252
			$args['changeset_uuid'] = wp_generate_uuid4();
253
		}
254
255
		// The theme and messenger_channel should be supplied via $args, but they are also looked at in the $_REQUEST global here for back-compat.
256
		if ( ! isset( $args['theme'] ) ) {
257
			if ( isset( $_REQUEST['customize_theme'] ) ) {
258
				$args['theme'] = wp_unslash( $_REQUEST['customize_theme'] );
259
			} elseif ( isset( $_REQUEST['theme'] ) ) { // Deprecated.
260
				$args['theme'] = wp_unslash( $_REQUEST['theme'] );
261
			}
262
		}
263
		if ( ! isset( $args['messenger_channel'] ) && isset( $_REQUEST['customize_messenger_channel'] ) ) {
264
			$args['messenger_channel'] = sanitize_key( wp_unslash( $_REQUEST['customize_messenger_channel'] ) );
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($_REQUEST['cu...ze_messenger_channel']) targeting wp_unslash() can also be of type array; however, sanitize_key() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
265
		}
266
267
		$this->original_stylesheet = get_stylesheet();
268
		$this->theme = wp_get_theme( $args['theme'] );
269
		$this->messenger_channel = $args['messenger_channel'];
270
		$this->_changeset_uuid = $args['changeset_uuid'];
271
272
		require_once( ABSPATH . WPINC . '/class-wp-customize-setting.php' );
273
		require_once( ABSPATH . WPINC . '/class-wp-customize-panel.php' );
274
		require_once( ABSPATH . WPINC . '/class-wp-customize-section.php' );
275
		require_once( ABSPATH . WPINC . '/class-wp-customize-control.php' );
276
277
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-color-control.php' );
278
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-media-control.php' );
279
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-upload-control.php' );
280
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-image-control.php' );
281
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-control.php' );
282
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-position-control.php' );
283
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-cropped-image-control.php' );
284
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-site-icon-control.php' );
285
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-control.php' );
286
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-theme-control.php' );
287
		require_once( ABSPATH . WPINC . '/customize/class-wp-widget-area-customize-control.php' );
288
		require_once( ABSPATH . WPINC . '/customize/class-wp-widget-form-customize-control.php' );
289
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-control.php' );
290
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-control.php' );
291
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-location-control.php' );
292
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-name-control.php' );
293
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-auto-add-control.php' );
294
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-control.php' );
295
296
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php' );
297
298
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php' );
299
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php' );
300
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-section.php' );
301
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-section.php' );
302
303
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-custom-css-setting.php' );
304
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-filter-setting.php' );
305
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-setting.php' );
306
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-setting.php' );
307
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-setting.php' );
308
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-setting.php' );
309
310
		/**
311
		 * Filters the core Customizer components to load.
312
		 *
313
		 * This allows Core components to be excluded from being instantiated by
314
		 * filtering them out of the array. Note that this filter generally runs
315
		 * during the {@see 'plugins_loaded'} action, so it cannot be added
316
		 * in a theme.
317
		 *
318
		 * @since 4.4.0
319
		 *
320
		 * @see WP_Customize_Manager::__construct()
321
		 *
322
		 * @param array                $components List of core components to load.
323
		 * @param WP_Customize_Manager $this       WP_Customize_Manager instance.
324
		 */
325
		$components = apply_filters( 'customize_loaded_components', $this->components, $this );
326
327
		require_once( ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php' );
328
		$this->selective_refresh = new WP_Customize_Selective_Refresh( $this );
329
330 View Code Duplication
		if ( in_array( 'widgets', $components, true ) ) {
331
			require_once( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
332
			$this->widgets = new WP_Customize_Widgets( $this );
333
		}
334
335 View Code Duplication
		if ( in_array( 'nav_menus', $components, true ) ) {
336
			require_once( ABSPATH . WPINC . '/class-wp-customize-nav-menus.php' );
337
			$this->nav_menus = new WP_Customize_Nav_Menus( $this );
338
		}
339
340
		add_action( 'setup_theme', array( $this, 'setup_theme' ) );
341
		add_action( 'wp_loaded',   array( $this, 'wp_loaded' ) );
342
343
		// Do not spawn cron (especially the alternate cron) while running the Customizer.
344
		remove_action( 'init', 'wp_cron' );
345
346
		// Do not run update checks when rendering the controls.
347
		remove_action( 'admin_init', '_maybe_update_core' );
348
		remove_action( 'admin_init', '_maybe_update_plugins' );
349
		remove_action( 'admin_init', '_maybe_update_themes' );
350
351
		add_action( 'wp_ajax_customize_save',           array( $this, 'save' ) );
352
		add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
353
354
		add_action( 'customize_register',                 array( $this, 'register_controls' ) );
355
		add_action( 'customize_register',                 array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first
356
		add_action( 'customize_controls_init',            array( $this, 'prepare_controls' ) );
357
		add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
358
359
		// Render Panel, Section, and Control templates.
360
		add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_panel_templates' ), 1 );
361
		add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_section_templates' ), 1 );
362
		add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_control_templates' ), 1 );
363
364
		// Export header video settings with the partial response.
365
		add_filter( 'customize_render_partials_response', array( $this, 'export_header_video_settings' ), 10, 3 );
366
367
		// Export the settings to JS via the _wpCustomizeSettings variable.
368
		add_action( 'customize_controls_print_footer_scripts', array( $this, 'customize_pane_settings' ), 1000 );
369
	}
370
371
	/**
372
	 * Return true if it's an Ajax request.
373
	 *
374
	 * @since 3.4.0
375
	 * @since 4.2.0 Added `$action` param.
376
	 * @access public
377
	 *
378
	 * @param string|null $action Whether the supplied Ajax action is being run.
379
	 * @return bool True if it's an Ajax request, false otherwise.
380
	 */
381
	public function doing_ajax( $action = null ) {
382
		if ( ! wp_doing_ajax() ) {
383
			return false;
384
		}
385
386
		if ( ! $action ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $action of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
387
			return true;
388
		} else {
389
			/*
390
			 * Note: we can't just use doing_action( "wp_ajax_{$action}" ) because we need
391
			 * to check before admin-ajax.php gets to that point.
392
			 */
393
			return isset( $_REQUEST['action'] ) && wp_unslash( $_REQUEST['action'] ) === $action;
394
		}
395
	}
396
397
	/**
398
	 * Custom wp_die wrapper. Returns either the standard message for UI
399
	 * or the Ajax message.
400
	 *
401
	 * @since 3.4.0
402
	 *
403
	 * @param mixed $ajax_message Ajax return
404
	 * @param mixed $message UI message
405
	 */
406
	protected function wp_die( $ajax_message, $message = null ) {
407
		if ( $this->doing_ajax() ) {
408
			wp_die( $ajax_message );
409
		}
410
411
		if ( ! $message ) {
412
			$message = __( 'Cheatin&#8217; uh?' );
413
		}
414
415
		if ( $this->messenger_channel ) {
416
			ob_start();
417
			wp_enqueue_scripts();
418
			wp_print_scripts( array( 'customize-base' ) );
419
420
			$settings = array(
421
				'messengerArgs' => array(
422
					'channel' => $this->messenger_channel,
423
					'url' => wp_customize_url(),
424
				),
425
				'error' => $ajax_message,
426
			);
427
			?>
428
			<script>
429
			( function( api, settings ) {
430
				var preview = new api.Messenger( settings.messengerArgs );
431
				preview.send( 'iframe-loading-error', settings.error );
432
			} )( wp.customize, <?php echo wp_json_encode( $settings ) ?> );
433
			</script>
434
			<?php
435
			$message .= ob_get_clean();
436
		}
437
438
		wp_die( $message );
439
	}
440
441
	/**
442
	 * Return the Ajax wp_die() handler if it's a customized request.
443
	 *
444
	 * @since 3.4.0
445
	 * @deprecated 4.7.0
446
	 *
447
	 * @return callable Die handler.
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use string.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
448
	 */
449
	public function wp_die_handler() {
450
		_deprecated_function( __METHOD__, '4.7.0' );
451
452
		if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
453
			return '_ajax_wp_die_handler';
454
		}
455
456
		return '_default_wp_die_handler';
457
	}
458
459
	/**
460
	 * Start preview and customize theme.
461
	 *
462
	 * Check if customize query variable exist. Init filters to filter the current theme.
463
	 *
464
	 * @since 3.4.0
465
	 *
466
	 * @global string $pagenow
467
	 */
468
	public function setup_theme() {
469
		global $pagenow;
470
471
		// Check permissions for customize.php access since this method is called before customize.php can run any code,
472 View Code Duplication
		if ( 'customize.php' === $pagenow && ! current_user_can( 'customize' ) ) {
473
			if ( ! is_user_logged_in() ) {
474
				auth_redirect();
475
			} else {
476
				wp_die(
477
					'<h1>' . __( 'Cheatin&#8217; uh?' ) . '</h1>' .
478
					'<p>' . __( 'Sorry, you are not allowed to customize this site.' ) . '</p>',
479
					403
480
				);
481
			}
482
			return;
483
		}
484
485
		if ( ! preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $this->_changeset_uuid ) ) {
486
			$this->wp_die( -1, __( 'Invalid changeset UUID' ) );
487
		}
488
489
		/*
490
		 * Clear incoming post data if the user lacks a CSRF token (nonce). Note that the customizer
491
		 * application will inject the customize_preview_nonce query parameter into all Ajax requests.
492
		 * For similar behavior elsewhere in WordPress, see rest_cookie_check_errors() which logs out
493
		 * a user when a valid nonce isn't present.
494
		 */
495
		$has_post_data_nonce = (
496
			check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'nonce', false )
497
			||
498
			check_ajax_referer( 'save-customize_' . $this->get_stylesheet(), 'nonce', false )
499
			||
500
			check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'customize_preview_nonce', false )
501
		);
502
		if ( ! current_user_can( 'customize' ) || ! $has_post_data_nonce ) {
503
			unset( $_POST['customized'] );
504
			unset( $_REQUEST['customized'] );
505
		}
506
507
		/*
508
		 * If unauthenticated then require a valid changeset UUID to load the preview.
509
		 * In this way, the UUID serves as a secret key. If the messenger channel is present,
510
		 * then send unauthenticated code to prompt re-auth.
511
		 */
512 View Code Duplication
		if ( ! current_user_can( 'customize' ) && ! $this->changeset_post_id() ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->changeset_post_id() of type null|integer is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
513
			$this->wp_die( $this->messenger_channel ? 0 : -1, __( 'Non-existent changeset UUID.' ) );
514
		}
515
516
		if ( ! headers_sent() ) {
517
			send_origin_headers();
518
		}
519
520
		// Hide the admin bar if we're embedded in the customizer iframe.
521
		if ( $this->messenger_channel ) {
522
			show_admin_bar( false );
523
		}
524
525
		if ( $this->is_theme_active() ) {
526
			// Once the theme is loaded, we'll validate it.
527
			add_action( 'after_setup_theme', array( $this, 'after_setup_theme' ) );
528
		} else {
529
			// If the requested theme is not the active theme and the user doesn't have the
530
			// switch_themes cap, bail.
531
			if ( ! current_user_can( 'switch_themes' ) ) {
532
				$this->wp_die( -1, __( 'Sorry, you are not allowed to edit theme options on this site.' ) );
533
			}
534
535
			// If the theme has errors while loading, bail.
536
			if ( $this->theme()->errors() ) {
537
				$this->wp_die( -1, $this->theme()->errors()->get_error_message() );
538
			}
539
540
			// If the theme isn't allowed per multisite settings, bail.
541
			if ( ! $this->theme()->is_allowed() ) {
542
				$this->wp_die( -1, __( 'The requested theme does not exist.' ) );
543
			}
544
		}
545
546
		/*
547
		 * Import theme starter content for fresh installs when landing in the customizer.
548
		 * Import starter content at after_setup_theme:100 so that any
549
		 * add_theme_support( 'starter-content' ) calls will have been made.
550
		 */
551
		if ( get_option( 'fresh_site' ) && 'customize.php' === $pagenow ) {
552
			add_action( 'after_setup_theme', array( $this, 'import_theme_starter_content' ), 100 );
553
		}
554
555
		$this->start_previewing_theme();
556
	}
557
558
	/**
559
	 * Callback to validate a theme once it is loaded
560
	 *
561
	 * @since 3.4.0
562
	 */
563
	public function after_setup_theme() {
564
		$doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_POST['customized'] ) );
565
		if ( ! $doing_ajax_or_is_customized && ! validate_current_theme() ) {
566
			wp_redirect( 'themes.php?broken=true' );
567
			exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method after_setup_theme() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
568
		}
569
	}
570
571
	/**
572
	 * If the theme to be previewed isn't the active theme, add filter callbacks
573
	 * to swap it out at runtime.
574
	 *
575
	 * @since 3.4.0
576
	 */
577 View Code Duplication
	public function start_previewing_theme() {
578
		// Bail if we're already previewing.
579
		if ( $this->is_preview() ) {
580
			return;
581
		}
582
583
		$this->previewing = true;
584
585
		if ( ! $this->is_theme_active() ) {
586
			add_filter( 'template', array( $this, 'get_template' ) );
587
			add_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
588
			add_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
589
590
			// @link: https://core.trac.wordpress.org/ticket/20027
591
			add_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
592
			add_filter( 'pre_option_template', array( $this, 'get_template' ) );
593
594
			// Handle custom theme roots.
595
			add_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
596
			add_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
597
		}
598
599
		/**
600
		 * Fires once the Customizer theme preview has started.
601
		 *
602
		 * @since 3.4.0
603
		 *
604
		 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
605
		 */
606
		do_action( 'start_previewing_theme', $this );
607
	}
608
609
	/**
610
	 * Stop previewing the selected theme.
611
	 *
612
	 * Removes filters to change the current theme.
613
	 *
614
	 * @since 3.4.0
615
	 */
616 View Code Duplication
	public function stop_previewing_theme() {
617
		if ( ! $this->is_preview() ) {
618
			return;
619
		}
620
621
		$this->previewing = false;
622
623
		if ( ! $this->is_theme_active() ) {
624
			remove_filter( 'template', array( $this, 'get_template' ) );
625
			remove_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
626
			remove_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
627
628
			// @link: https://core.trac.wordpress.org/ticket/20027
629
			remove_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
630
			remove_filter( 'pre_option_template', array( $this, 'get_template' ) );
631
632
			// Handle custom theme roots.
633
			remove_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
634
			remove_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
635
		}
636
637
		/**
638
		 * Fires once the Customizer theme preview has stopped.
639
		 *
640
		 * @since 3.4.0
641
		 *
642
		 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
643
		 */
644
		do_action( 'stop_previewing_theme', $this );
645
	}
646
647
	/**
648
	 * Get the changeset UUID.
649
	 *
650
	 * @since 4.7.0
651
	 * @access public
652
	 *
653
	 * @return string UUID.
654
	 */
655
	public function changeset_uuid() {
656
		return $this->_changeset_uuid;
657
	}
658
659
	/**
660
	 * Get the theme being customized.
661
	 *
662
	 * @since 3.4.0
663
	 *
664
	 * @return WP_Theme
665
	 */
666
	public function theme() {
667
		if ( ! $this->theme ) {
668
			$this->theme = wp_get_theme();
669
		}
670
		return $this->theme;
671
	}
672
673
	/**
674
	 * Get the registered settings.
675
	 *
676
	 * @since 3.4.0
677
	 *
678
	 * @return array
679
	 */
680
	public function settings() {
681
		return $this->settings;
682
	}
683
684
	/**
685
	 * Get the registered controls.
686
	 *
687
	 * @since 3.4.0
688
	 *
689
	 * @return array
690
	 */
691
	public function controls() {
692
		return $this->controls;
693
	}
694
695
	/**
696
	 * Get the registered containers.
697
	 *
698
	 * @since 4.0.0
699
	 *
700
	 * @return array
701
	 */
702
	public function containers() {
703
		return $this->containers;
704
	}
705
706
	/**
707
	 * Get the registered sections.
708
	 *
709
	 * @since 3.4.0
710
	 *
711
	 * @return array
712
	 */
713
	public function sections() {
714
		return $this->sections;
715
	}
716
717
	/**
718
	 * Get the registered panels.
719
	 *
720
	 * @since 4.0.0
721
	 * @access public
722
	 *
723
	 * @return array Panels.
724
	 */
725
	public function panels() {
726
		return $this->panels;
727
	}
728
729
	/**
730
	 * Checks if the current theme is active.
731
	 *
732
	 * @since 3.4.0
733
	 *
734
	 * @return bool
735
	 */
736
	public function is_theme_active() {
737
		return $this->get_stylesheet() == $this->original_stylesheet;
738
	}
739
740
	/**
741
	 * Register styles/scripts and initialize the preview of each setting
742
	 *
743
	 * @since 3.4.0
744
	 */
745
	public function wp_loaded() {
746
747
		/**
748
		 * Fires once WordPress has loaded, allowing scripts and styles to be initialized.
749
		 *
750
		 * @since 3.4.0
751
		 *
752
		 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
753
		 */
754
		do_action( 'customize_register', $this );
755
756
		/*
757
		 * Note that settings must be previewed here even outside the customizer preview
758
		 * and also in the customizer pane itself. This is to enable loading an existing
759
		 * changeset into the customizer. Previewing the settings only has to be prevented
760
		 * in the case of a customize_save action because then update_option()
761
		 * may short-circuit because it will detect that there are no changes to
762
		 * make.
763
		 */
764
		if ( ! $this->doing_ajax( 'customize_save' ) ) {
765
			foreach ( $this->settings as $setting ) {
766
				$setting->preview();
767
			}
768
		}
769
770
		if ( $this->is_preview() && ! is_admin() ) {
771
			$this->customize_preview_init();
772
		}
773
	}
774
775
	/**
776
	 * Prevents Ajax requests from following redirects when previewing a theme
777
	 * by issuing a 200 response instead of a 30x.
778
	 *
779
	 * Instead, the JS will sniff out the location header.
780
	 *
781
	 * @since 3.4.0
782
	 * @deprecated 4.7.0
783
	 *
784
	 * @param int $status Status.
785
	 * @return int
786
	 */
787
	public function wp_redirect_status( $status ) {
788
		_deprecated_function( __FUNCTION__, '4.7.0' );
789
790
		if ( $this->is_preview() && ! is_admin() ) {
791
			return 200;
792
		}
793
794
		return $status;
795
	}
796
797
	/**
798
	 * Find the changeset post ID for a given changeset UUID.
799
	 *
800
	 * @since 4.7.0
801
	 * @access public
802
	 *
803
	 * @param string $uuid Changeset UUID.
804
	 * @return int|null Returns post ID on success and null on failure.
805
	 */
806
	public function find_changeset_post_id( $uuid ) {
807
		$cache_group = 'customize_changeset_post';
808
		$changeset_post_id = wp_cache_get( $uuid, $cache_group );
809
		if ( $changeset_post_id && 'customize_changeset' === get_post_type( $changeset_post_id ) ) {
810
			return $changeset_post_id;
811
		}
812
813
		$changeset_post_query = new WP_Query( array(
814
			'post_type' => 'customize_changeset',
815
			'post_status' => get_post_stati(),
816
			'name' => $uuid,
817
			'posts_per_page' => 1,
818
			'no_found_rows' => true,
819
			'cache_results' => true,
820
			'update_post_meta_cache' => false,
821
			'update_post_term_cache' => false,
822
			'lazy_load_term_meta' => false,
823
		) );
824
		if ( ! empty( $changeset_post_query->posts ) ) {
825
			// Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed.
826
			$changeset_post_id = $changeset_post_query->posts[0]->ID;
827
			wp_cache_set( $this->_changeset_uuid, $changeset_post_id, $cache_group );
828
			return $changeset_post_id;
829
		}
830
831
		return null;
832
	}
833
834
	/**
835
	 * Get the changeset post id for the loaded changeset.
836
	 *
837
	 * @since 4.7.0
838
	 * @access public
839
	 *
840
	 * @return int|null Post ID on success or null if there is no post yet saved.
841
	 */
842
	public function changeset_post_id() {
843
		if ( ! isset( $this->_changeset_post_id ) ) {
844
			$post_id = $this->find_changeset_post_id( $this->_changeset_uuid );
845
			if ( ! $post_id ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $post_id of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
846
				$post_id = false;
847
			}
848
			$this->_changeset_post_id = $post_id;
849
		}
850
		if ( false === $this->_changeset_post_id ) {
851
			return null;
852
		}
853
		return $this->_changeset_post_id;
854
	}
855
856
	/**
857
	 * Get the data stored in a changeset post.
858
	 *
859
	 * @since 4.7.0
860
	 * @access protected
861
	 *
862
	 * @param int $post_id Changeset post ID.
863
	 * @return array|WP_Error Changeset data or WP_Error on error.
864
	 */
865
	protected function get_changeset_post_data( $post_id ) {
866
		if ( ! $post_id ) {
867
			return new WP_Error( 'empty_post_id' );
868
		}
869
		$changeset_post = get_post( $post_id );
870
		if ( ! $changeset_post ) {
871
			return new WP_Error( 'missing_post' );
872
		}
873
		if ( 'customize_changeset' !== $changeset_post->post_type ) {
874
			return new WP_Error( 'wrong_post_type' );
875
		}
876
		$changeset_data = json_decode( $changeset_post->post_content, true );
877
		if ( function_exists( 'json_last_error' ) && json_last_error() ) {
878
			return new WP_Error( 'json_parse_error', '', json_last_error() );
879
		}
880
		if ( ! is_array( $changeset_data ) ) {
881
			return new WP_Error( 'expected_array' );
882
		}
883
		return $changeset_data;
884
	}
885
886
	/**
887
	 * Get changeset data.
888
	 *
889
	 * @since 4.7.0
890
	 * @access public
891
	 *
892
	 * @return array Changeset data.
0 ignored issues
show
Documentation introduced by
Should the return type not be array|WP_Error? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
893
	 */
894
	public function changeset_data() {
895
		if ( isset( $this->_changeset_data ) ) {
896
			return $this->_changeset_data;
897
		}
898
		$changeset_post_id = $this->changeset_post_id();
899
		if ( ! $changeset_post_id ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $changeset_post_id of type null|integer is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
900
			$this->_changeset_data = array();
901
		} else {
902
			$data = $this->get_changeset_post_data( $changeset_post_id );
903
			if ( ! is_wp_error( $data ) ) {
904
				$this->_changeset_data = $data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $data can also be of type object<WP_Error>. However, the property $_changeset_data is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
905
			} else {
906
				$this->_changeset_data = array();
907
			}
908
		}
909
		return $this->_changeset_data;
910
	}
911
912
	/**
913
	 * Starter content setting IDs.
914
	 *
915
	 * @since 4.7.0
916
	 * @access private
917
	 * @var array
918
	 */
919
	protected $pending_starter_content_settings_ids = array();
920
921
	/**
922
	 * Import theme starter content into the customized state.
923
	 *
924
	 * @since 4.7.0
925
	 * @access public
926
	 *
927
	 * @param array $starter_content Starter content. Defaults to `get_theme_starter_content()`.
928
	 */
929
	function import_theme_starter_content( $starter_content = array() ) {
930
		if ( empty( $starter_content ) ) {
931
			$starter_content = get_theme_starter_content();
932
		}
933
934
		$changeset_data = array();
935
		if ( $this->changeset_post_id() ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->changeset_post_id() of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
936
			$changeset_data = $this->get_changeset_post_data( $this->changeset_post_id() );
937
		}
938
939
		$sidebars_widgets = isset( $starter_content['widgets'] ) && ! empty( $this->widgets ) ? $starter_content['widgets'] : array();
940
		$attachments = isset( $starter_content['attachments'] ) && ! empty( $this->nav_menus ) ? $starter_content['attachments'] : array();
941
		$posts = isset( $starter_content['posts'] ) && ! empty( $this->nav_menus ) ? $starter_content['posts'] : array();
942
		$options = isset( $starter_content['options'] ) ? $starter_content['options'] : array();
943
		$nav_menus = isset( $starter_content['nav_menus'] ) && ! empty( $this->nav_menus ) ? $starter_content['nav_menus'] : array();
944
		$theme_mods = isset( $starter_content['theme_mods'] ) ? $starter_content['theme_mods'] : array();
945
946
		// Widgets.
947
		$max_widget_numbers = array();
948
		foreach ( $sidebars_widgets as $sidebar_id => $widgets ) {
949
			$sidebar_widget_ids = array();
950
			foreach ( $widgets as $widget ) {
951
				list( $id_base, $instance ) = $widget;
952
953
				if ( ! isset( $max_widget_numbers[ $id_base ] ) ) {
954
955
					// When $settings is an array-like object, get an intrinsic array for use with array_keys().
956
					$settings = get_option( "widget_{$id_base}", array() );
957
					if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) {
958
						$settings = $settings->getArrayCopy();
959
					}
960
961
					// Find the max widget number for this type.
962
					$widget_numbers = array_keys( $settings );
963
					if ( count( $widget_numbers ) > 0 ) {
964
						$widget_numbers[] = 1;
965
						$max_widget_numbers[ $id_base ] = call_user_func_array( 'max', $widget_numbers );
966
					} else {
967
						$max_widget_numbers[ $id_base ] = 1;
968
					}
969
				}
970
				$max_widget_numbers[ $id_base ] += 1;
971
972
				$widget_id = sprintf( '%s-%d', $id_base, $max_widget_numbers[ $id_base ] );
973
				$setting_id = sprintf( 'widget_%s[%d]', $id_base, $max_widget_numbers[ $id_base ] );
974
975
				$setting_value = $this->widgets->sanitize_widget_js_instance( $instance );
976 View Code Duplication
				if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
977
					$this->set_post_value( $setting_id, $setting_value );
978
					$this->pending_starter_content_settings_ids[] = $setting_id;
979
				}
980
				$sidebar_widget_ids[] = $widget_id;
981
			}
982
983
			$setting_id = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
984 View Code Duplication
			if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
985
				$this->set_post_value( $setting_id, $sidebar_widget_ids );
986
				$this->pending_starter_content_settings_ids[] = $setting_id;
987
			}
988
		}
989
990
		$starter_content_auto_draft_post_ids = array();
991
		if ( ! empty( $changeset_data['nav_menus_created_posts']['value'] ) ) {
992
			$starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, $changeset_data['nav_menus_created_posts']['value'] );
993
		}
994
995
		// Make an index of all the posts needed and what their slugs are.
996
		$needed_posts = array();
997
		$attachments = $this->prepare_starter_content_attachments( $attachments );
998
		foreach ( $attachments as $attachment ) {
999
			$key = 'attachment:' . $attachment['post_name'];
1000
			$needed_posts[ $key ] = true;
1001
		}
1002
		foreach ( array_keys( $posts ) as $post_symbol ) {
1003
			if ( empty( $posts[ $post_symbol ]['post_name'] ) && empty( $posts[ $post_symbol ]['post_title'] ) ) {
1004
				unset( $posts[ $post_symbol ] );
1005
				continue;
1006
			}
1007 View Code Duplication
			if ( empty( $posts[ $post_symbol ]['post_name'] ) ) {
1008
				$posts[ $post_symbol ]['post_name'] = sanitize_title( $posts[ $post_symbol ]['post_title'] );
1009
			}
1010
			if ( empty( $posts[ $post_symbol ]['post_type'] ) ) {
1011
				$posts[ $post_symbol ]['post_type'] = 'post';
1012
			}
1013
			$needed_posts[ $posts[ $post_symbol ]['post_type'] . ':' . $posts[ $post_symbol ]['post_name'] ] = true;
1014
		}
1015
		$all_post_slugs = array_merge(
1016
			wp_list_pluck( $attachments, 'post_name' ),
1017
			wp_list_pluck( $posts, 'post_name' )
1018
		);
1019
1020
		/*
1021
		 * Obtain all post types referenced in starter content to use in query.
1022
		 * This is needed because 'any' will not account for post types not yet registered.
1023
		 */
1024
		$post_types = array_filter( array_merge( array( 'attachment' ), wp_list_pluck( $posts, 'post_type' ) ) );
1025
1026
		// Re-use auto-draft starter content posts referenced in the current customized state.
1027
		$existing_starter_content_posts = array();
1028
		if ( ! empty( $starter_content_auto_draft_post_ids ) ) {
1029
			$existing_posts_query = new WP_Query( array(
1030
				'post__in' => $starter_content_auto_draft_post_ids,
1031
				'post_status' => 'auto-draft',
1032
				'post_type' => $post_types,
1033
				'posts_per_page' => -1,
1034
			) );
1035
			foreach ( $existing_posts_query->posts as $existing_post ) {
1036
				$post_name = $existing_post->post_name;
1037
				if ( empty( $post_name ) ) {
1038
					$post_name = get_post_meta( $existing_post->ID, '_customize_draft_post_name', true );
1039
				}
1040
				$existing_starter_content_posts[ $existing_post->post_type . ':' . $post_name ] = $existing_post;
1041
			}
1042
		}
1043
1044
		// Re-use non-auto-draft posts.
1045
		if ( ! empty( $all_post_slugs ) ) {
1046
			$existing_posts_query = new WP_Query( array(
1047
				'post_name__in' => $all_post_slugs,
1048
				'post_status' => array_diff( get_post_stati(), array( 'auto-draft' ) ),
1049
				'post_type' => 'any',
1050
				'posts_per_page' => -1,
1051
			) );
1052
			foreach ( $existing_posts_query->posts as $existing_post ) {
1053
				$key = $existing_post->post_type . ':' . $existing_post->post_name;
1054
				if ( isset( $needed_posts[ $key ] ) && ! isset( $existing_starter_content_posts[ $key ] ) ) {
1055
					$existing_starter_content_posts[ $key ] = $existing_post;
1056
				}
1057
			}
1058
		}
1059
1060
		// Attachments are technically posts but handled differently.
1061
		if ( ! empty( $attachments ) ) {
1062
1063
			$attachment_ids = array();
1064
1065
			foreach ( $attachments as $symbol => $attachment ) {
1066
				$file_array = array(
1067
					'name' => $attachment['file_name'],
1068
				);
1069
				$file_path = $attachment['file_path'];
1070
				$attachment_id = null;
1071
				$attached_file = null;
1072
				if ( isset( $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ] ) ) {
1073
					$attachment_post = $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ];
1074
					$attachment_id = $attachment_post->ID;
1075
					$attached_file = get_attached_file( $attachment_id );
1076
					if ( empty( $attached_file ) || ! file_exists( $attached_file ) ) {
1077
						$attachment_id = null;
1078
						$attached_file = null;
1079
					} elseif ( $this->get_stylesheet() !== get_post_meta( $attachment_post->ID, '_starter_content_theme', true ) ) {
1080
1081
						// Re-generate attachment metadata since it was previously generated for a different theme.
1082
						$metadata = wp_generate_attachment_metadata( $attachment_post->ID, $attached_file );
1083
						wp_update_attachment_metadata( $attachment_id, $metadata );
1084
						update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
1085
					}
1086
				}
1087
1088
				// Insert the attachment auto-draft because it doesn't yet exist or the attached file is gone.
1089
				if ( ! $attachment_id ) {
1090
1091
					// Copy file to temp location so that original file won't get deleted from theme after sideloading.
1092
					$temp_file_name = wp_tempnam( basename( $file_path ) );
1093
					if ( $temp_file_name && copy( $file_path, $temp_file_name ) ) {
1094
						$file_array['tmp_name'] = $temp_file_name;
1095
					}
1096
					if ( empty( $file_array['tmp_name'] ) ) {
1097
						continue;
1098
					}
1099
1100
					$attachment_post_data = array_merge(
1101
						wp_array_slice_assoc( $attachment, array( 'post_title', 'post_content', 'post_excerpt' ) ),
1102
						array(
1103
							'post_status' => 'auto-draft', // So attachment will be garbage collected in a week if changeset is never published.
1104
						)
1105
					);
1106
1107
					// In PHP < 5.6 filesize() returns 0 for the temp files unless we clear the file status cache.
1108
					// Technically, PHP < 5.6.0 || < 5.5.13 || < 5.4.29 but no need to be so targeted.
1109
					// See https://bugs.php.net/bug.php?id=65701
1110
					if ( version_compare( PHP_VERSION, '5.6', '<' ) ) {
1111
						clearstatcache();
1112
					}
1113
1114
					$attachment_id = media_handle_sideload( $file_array, 0, null, $attachment_post_data );
1115
					if ( is_wp_error( $attachment_id ) ) {
1116
						continue;
1117
					}
1118
					update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
1119
					update_post_meta( $attachment_id, '_customize_draft_post_name', $attachment['post_name'] );
1120
				}
1121
1122
				$attachment_ids[ $symbol ] = $attachment_id;
1123
			}
1124
			$starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, array_values( $attachment_ids ) );
1125
		}
1126
1127
		// Posts & pages.
1128
		if ( ! empty( $posts ) ) {
1129
			foreach ( array_keys( $posts ) as $post_symbol ) {
1130
				if ( empty( $posts[ $post_symbol ]['post_type'] ) || empty( $posts[ $post_symbol ]['post_name'] ) ) {
1131
					continue;
1132
				}
1133
				$post_type = $posts[ $post_symbol ]['post_type'];
1134
				if ( ! empty( $posts[ $post_symbol ]['post_name'] ) ) {
1135
					$post_name = $posts[ $post_symbol ]['post_name'];
1136 View Code Duplication
				} elseif ( ! empty( $posts[ $post_symbol ]['post_title'] ) ) {
1137
					$post_name = sanitize_title( $posts[ $post_symbol ]['post_title'] );
1138
				} else {
1139
					continue;
1140
				}
1141
1142
				// Use existing auto-draft post if one already exists with the same type and name.
1143
				if ( isset( $existing_starter_content_posts[ $post_type . ':' . $post_name ] ) ) {
1144
					$posts[ $post_symbol ]['ID'] = $existing_starter_content_posts[ $post_type . ':' . $post_name ]->ID;
1145
					continue;
1146
				}
1147
1148
				// Translate the featured image symbol.
1149
				if ( ! empty( $posts[ $post_symbol ]['thumbnail'] )
1150
					&& preg_match( '/^{{(?P<symbol>.+)}}$/', $posts[ $post_symbol ]['thumbnail'], $matches )
1151
					&& isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1152
					$posts[ $post_symbol ]['meta_input']['_thumbnail_id'] = $attachment_ids[ $matches['symbol'] ];
1153
				}
1154
1155
				if ( ! empty( $posts[ $post_symbol ]['template'] ) ) {
1156
					$posts[ $post_symbol ]['meta_input']['_wp_page_template'] = $posts[ $post_symbol ]['template'];
1157
				}
1158
1159
				$r = $this->nav_menus->insert_auto_draft_post( $posts[ $post_symbol ] );
1160
				if ( $r instanceof WP_Post ) {
1161
					$posts[ $post_symbol ]['ID'] = $r->ID;
1162
				}
1163
			}
1164
1165
			$starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, wp_list_pluck( $posts, 'ID' ) );
1166
		}
1167
1168
		// The nav_menus_created_posts setting is why nav_menus component is dependency for adding posts.
1169
		if ( ! empty( $this->nav_menus ) && ! empty( $starter_content_auto_draft_post_ids ) ) {
1170
			$setting_id = 'nav_menus_created_posts';
1171
			$this->set_post_value( $setting_id, array_unique( array_values( $starter_content_auto_draft_post_ids ) ) );
1172
			$this->pending_starter_content_settings_ids[] = $setting_id;
1173
		}
1174
1175
		// Nav menus.
1176
		$placeholder_id = -1;
1177
		$reused_nav_menu_setting_ids = array();
1178
		foreach ( $nav_menus as $nav_menu_location => $nav_menu ) {
1179
1180
			$nav_menu_term_id = null;
1181
			$nav_menu_setting_id = null;
1182
			$matches = array();
1183
1184
			// Look for an existing placeholder menu with starter content to re-use.
1185
			foreach ( $changeset_data as $setting_id => $setting_params ) {
0 ignored issues
show
Bug introduced by
The expression $changeset_data of type object<WP_Error>|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1186
				$can_reuse = (
1187
					! empty( $setting_params['starter_content'] )
1188
					&&
1189
					! in_array( $setting_id, $reused_nav_menu_setting_ids, true )
1190
					&&
1191
					preg_match( '#^nav_menu\[(?P<nav_menu_id>-?\d+)\]$#', $setting_id, $matches )
1192
				);
1193
				if ( $can_reuse ) {
1194
					$nav_menu_term_id = intval( $matches['nav_menu_id'] );
1195
					$nav_menu_setting_id = $setting_id;
1196
					$reused_nav_menu_setting_ids[] = $setting_id;
1197
					break;
1198
				}
1199
			}
1200
1201
			if ( ! $nav_menu_term_id ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $nav_menu_term_id of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1202
				while ( isset( $changeset_data[ sprintf( 'nav_menu[%d]', $placeholder_id ) ] ) ) {
1203
					$placeholder_id--;
1204
				}
1205
				$nav_menu_term_id = $placeholder_id;
1206
				$nav_menu_setting_id = sprintf( 'nav_menu[%d]', $placeholder_id );
1207
			}
1208
1209
			$this->set_post_value( $nav_menu_setting_id, array(
1210
				'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location,
1211
			) );
1212
			$this->pending_starter_content_settings_ids[] = $nav_menu_setting_id;
1213
1214
			// @todo Add support for menu_item_parent.
1215
			$position = 0;
1216
			foreach ( $nav_menu['items'] as $nav_menu_item ) {
1217
				$nav_menu_item_setting_id = sprintf( 'nav_menu_item[%d]', $placeholder_id-- );
1218
				if ( ! isset( $nav_menu_item['position'] ) ) {
1219
					$nav_menu_item['position'] = $position++;
1220
				}
1221
				$nav_menu_item['nav_menu_term_id'] = $nav_menu_term_id;
1222
1223
				if ( isset( $nav_menu_item['object_id'] ) ) {
1224
					if ( 'post_type' === $nav_menu_item['type'] && preg_match( '/^{{(?P<symbol>.+)}}$/', $nav_menu_item['object_id'], $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
1225
						$nav_menu_item['object_id'] = $posts[ $matches['symbol'] ]['ID'];
1226
						if ( empty( $nav_menu_item['title'] ) ) {
1227
							$original_object = get_post( $nav_menu_item['object_id'] );
1228
							$nav_menu_item['title'] = $original_object->post_title;
1229
						}
1230
					} else {
1231
						continue;
1232
					}
1233
				} else {
1234
					$nav_menu_item['object_id'] = 0;
1235
				}
1236
1237 View Code Duplication
				if ( empty( $changeset_data[ $nav_menu_item_setting_id ] ) || ! empty( $changeset_data[ $nav_menu_item_setting_id ]['starter_content'] ) ) {
1238
					$this->set_post_value( $nav_menu_item_setting_id, $nav_menu_item );
1239
					$this->pending_starter_content_settings_ids[] = $nav_menu_item_setting_id;
1240
				}
1241
			}
1242
1243
			$setting_id = sprintf( 'nav_menu_locations[%s]', $nav_menu_location );
1244 View Code Duplication
			if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
1245
				$this->set_post_value( $setting_id, $nav_menu_term_id );
1246
				$this->pending_starter_content_settings_ids[] = $setting_id;
1247
			}
1248
		}
1249
1250
		// Options.
1251
		foreach ( $options as $name => $value ) {
1252 View Code Duplication
			if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
1253
				if ( isset( $posts[ $matches['symbol'] ] ) ) {
1254
					$value = $posts[ $matches['symbol'] ]['ID'];
1255
				} elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1256
					$value = $attachment_ids[ $matches['symbol'] ];
1257
				} else {
1258
					continue;
1259
				}
1260
			}
1261
1262 View Code Duplication
			if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
1263
				$this->set_post_value( $name, $value );
1264
				$this->pending_starter_content_settings_ids[] = $name;
1265
			}
1266
		}
1267
1268
		// Theme mods.
1269
		foreach ( $theme_mods as $name => $value ) {
1270 View Code Duplication
			if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
1271
				if ( isset( $posts[ $matches['symbol'] ] ) ) {
1272
					$value = $posts[ $matches['symbol'] ]['ID'];
1273
				} elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1274
					$value = $attachment_ids[ $matches['symbol'] ];
1275
				} else {
1276
					continue;
1277
				}
1278
			}
1279
1280
			// Handle header image as special case since setting has a legacy format.
1281
			if ( 'header_image' === $name ) {
1282
				$name = 'header_image_data';
1283
				$metadata = wp_get_attachment_metadata( $value );
1284
				if ( empty( $metadata ) ) {
1285
					continue;
1286
				}
1287
				$value = array(
1288
					'attachment_id' => $value,
1289
					'url' => wp_get_attachment_url( $value ),
1290
					'height' => $metadata['height'],
1291
					'width' => $metadata['width'],
1292
				);
1293
			} elseif ( 'background_image' === $name ) {
1294
				$value = wp_get_attachment_url( $value );
1295
			}
1296
1297 View Code Duplication
			if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
1298
				$this->set_post_value( $name, $value );
1299
				$this->pending_starter_content_settings_ids[] = $name;
1300
			}
1301
		}
1302
1303
		if ( ! empty( $this->pending_starter_content_settings_ids ) ) {
1304
			if ( did_action( 'customize_register' ) ) {
1305
				$this->_save_starter_content_changeset();
1306
			} else {
1307
				add_action( 'customize_register', array( $this, '_save_starter_content_changeset' ), 1000 );
1308
			}
1309
		}
1310
	}
1311
1312
	/**
1313
	 * Prepare starter content attachments.
1314
	 *
1315
	 * Ensure that the attachments are valid and that they have slugs and file name/path.
1316
	 *
1317
	 * @since 4.7.0
1318
	 * @access private
1319
	 *
1320
	 * @param array $attachments Attachments.
1321
	 * @return array Prepared attachments.
1322
	 */
1323
	protected function prepare_starter_content_attachments( $attachments ) {
1324
		$prepared_attachments = array();
1325
		if ( empty( $attachments ) ) {
1326
			return $prepared_attachments;
1327
		}
1328
1329
		// Such is The WordPress Way.
1330
		require_once( ABSPATH . 'wp-admin/includes/file.php' );
1331
		require_once( ABSPATH . 'wp-admin/includes/media.php' );
1332
		require_once( ABSPATH . 'wp-admin/includes/image.php' );
1333
1334
		foreach ( $attachments as $symbol => $attachment ) {
1335
1336
			// A file is required and URLs to files are not currently allowed.
1337
			if ( empty( $attachment['file'] ) || preg_match( '#^https?://$#', $attachment['file'] ) ) {
1338
				continue;
1339
			}
1340
1341
			$file_path = null;
1342
			if ( file_exists( $attachment['file'] ) ) {
1343
				$file_path = $attachment['file']; // Could be absolute path to file in plugin.
1344
			} elseif ( is_child_theme() && file_exists( get_stylesheet_directory() . '/' . $attachment['file'] ) ) {
1345
				$file_path = get_stylesheet_directory() . '/' . $attachment['file'];
1346
			} elseif ( file_exists( get_template_directory() . '/' . $attachment['file'] ) ) {
1347
				$file_path = get_template_directory() . '/' . $attachment['file'];
1348
			} else {
1349
				continue;
1350
			}
1351
			$file_name = basename( $attachment['file'] );
1352
1353
			// Skip file types that are not recognized.
1354
			$checked_filetype = wp_check_filetype( $file_name );
1355
			if ( empty( $checked_filetype['type'] ) ) {
1356
				continue;
1357
			}
1358
1359
			// Ensure post_name is set since not automatically derived from post_title for new auto-draft posts.
1360
			if ( empty( $attachment['post_name'] ) ) {
1361
				if ( ! empty( $attachment['post_title'] ) ) {
1362
					$attachment['post_name'] = sanitize_title( $attachment['post_title'] );
1363
				} else {
1364
					$attachment['post_name'] = sanitize_title( preg_replace( '/\.\w+$/', '', $file_name ) );
1365
				}
1366
			}
1367
1368
			$attachment['file_name'] = $file_name;
1369
			$attachment['file_path'] = $file_path;
1370
			$prepared_attachments[ $symbol ] = $attachment;
1371
		}
1372
		return $prepared_attachments;
1373
	}
1374
1375
	/**
1376
	 * Save starter content changeset.
1377
	 *
1378
	 * @since 4.7.0
1379
	 * @access private
1380
	 */
1381
	public function _save_starter_content_changeset() {
1382
1383
		if ( empty( $this->pending_starter_content_settings_ids ) ) {
1384
			return;
1385
		}
1386
1387
		$this->save_changeset_post( array(
1388
			'data' => array_fill_keys( $this->pending_starter_content_settings_ids, array( 'starter_content' => true ) ),
1389
			'starter_content' => true,
1390
		) );
1391
1392
		$this->pending_starter_content_settings_ids = array();
1393
	}
1394
1395
	/**
1396
	 * Get dirty pre-sanitized setting values in the current customized state.
1397
	 *
1398
	 * The returned array consists of a merge of three sources:
1399
	 * 1. If the theme is not currently active, then the base array is any stashed
1400
	 *    theme mods that were modified previously but never published.
1401
	 * 2. The values from the current changeset, if it exists.
1402
	 * 3. If the user can customize, the values parsed from the incoming
1403
	 *    `$_POST['customized']` JSON data.
1404
	 * 4. Any programmatically-set post values via `WP_Customize_Manager::set_post_value()`.
1405
	 *
1406
	 * The name "unsanitized_post_values" is a carry-over from when the customized
1407
	 * state was exclusively sourced from `$_POST['customized']`. Nevertheless,
1408
	 * the value returned will come from the current changeset post and from the
1409
	 * incoming post data.
1410
	 *
1411
	 * @since 4.1.1
1412
	 * @since 4.7.0 Added $args param and merging with changeset values and stashed theme mods.
1413
	 *
1414
	 * @param array $args {
1415
	 *     Args.
1416
	 *
1417
	 *     @type bool $exclude_changeset Whether the changeset values should also be excluded. Defaults to false.
1418
	 *     @type bool $exclude_post_data Whether the post input values should also be excluded. Defaults to false when lacking the customize capability.
1419
	 * }
1420
	 * @return array
1421
	 */
1422
	public function unsanitized_post_values( $args = array() ) {
1423
		$args = array_merge(
1424
			array(
1425
				'exclude_changeset' => false,
1426
				'exclude_post_data' => ! current_user_can( 'customize' ),
1427
			),
1428
			$args
1429
		);
1430
1431
		$values = array();
1432
1433
		// Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present.
1434
		if ( ! $this->is_theme_active() ) {
1435
			$stashed_theme_mods = get_option( 'customize_stashed_theme_mods' );
1436
			$stylesheet = $this->get_stylesheet();
1437
			if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) {
1438
				$values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) );
1439
			}
1440
		}
1441
1442
		if ( ! $args['exclude_changeset'] ) {
1443
			foreach ( $this->changeset_data() as $setting_id => $setting_params ) {
0 ignored issues
show
Bug introduced by
The expression $this->changeset_data() of type array|object<WP_Error> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1444
				if ( ! array_key_exists( 'value', $setting_params ) ) {
1445
					continue;
1446
				}
1447
				if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) {
1448
1449
					// Ensure that theme mods values are only used if they were saved under the current theme.
1450
					$namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
1451
					if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) {
1452
						$values[ $matches['setting_id'] ] = $setting_params['value'];
1453
					}
1454
				} else {
1455
					$values[ $setting_id ] = $setting_params['value'];
1456
				}
1457
			}
1458
		}
1459
1460
		if ( ! $args['exclude_post_data'] ) {
1461
			if ( ! isset( $this->_post_values ) ) {
1462
				if ( isset( $_POST['customized'] ) ) {
1463
					$post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($_POST['customized']) targeting wp_unslash() can also be of type array; however, json_decode() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1464
				} else {
1465
					$post_values = array();
1466
				}
1467
				if ( is_array( $post_values ) ) {
1468
					$this->_post_values = $post_values;
1469
				} else {
1470
					$this->_post_values = array();
1471
				}
1472
			}
1473
			$values = array_merge( $values, $this->_post_values );
1474
		}
1475
		return $values;
1476
	}
1477
1478
	/**
1479
	 * Returns the sanitized value for a given setting from the current customized state.
1480
	 *
1481
	 * The name "post_value" is a carry-over from when the customized state was exclusively
1482
	 * sourced from `$_POST['customized']`. Nevertheless, the value returned will come
1483
	 * from the current changeset post and from the incoming post data.
1484
	 *
1485
	 * @since 3.4.0
1486
	 * @since 4.1.1 Introduced the `$default` parameter.
1487
	 * @since 4.6.0 `$default` is now returned early when the setting post value is invalid.
1488
	 * @access public
1489
	 *
1490
	 * @see WP_REST_Server::dispatch()
1491
	 * @see WP_Rest_Request::sanitize_params()
1492
	 * @see WP_Rest_Request::has_valid_params()
1493
	 *
1494
	 * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object.
1495
	 * @param mixed                $default Value returned $setting has no post value (added in 4.2.0)
1496
	 *                                      or the post value is invalid (added in 4.6.0).
1497
	 * @return string|mixed $post_value Sanitized value or the $default provided.
1498
	 */
1499
	public function post_value( $setting, $default = null ) {
1500
		$post_values = $this->unsanitized_post_values();
1501
		if ( ! array_key_exists( $setting->id, $post_values ) ) {
1502
			return $default;
1503
		}
1504
		$value = $post_values[ $setting->id ];
1505
		$valid = $setting->validate( $value );
1506
		if ( is_wp_error( $valid ) ) {
1507
			return $default;
1508
		}
1509
		$value = $setting->sanitize( $value );
1510
		if ( is_null( $value ) || is_wp_error( $value ) ) {
1511
			return $default;
1512
		}
1513
		return $value;
1514
	}
1515
1516
	/**
1517
	 * Override a setting's value in the current customized state.
1518
	 *
1519
	 * The name "post_value" is a carry-over from when the customized state was
1520
	 * exclusively sourced from `$_POST['customized']`.
1521
	 *
1522
	 * @since 4.2.0
1523
	 * @access public
1524
	 *
1525
	 * @param string $setting_id ID for the WP_Customize_Setting instance.
1526
	 * @param mixed  $value      Post value.
1527
	 */
1528
	public function set_post_value( $setting_id, $value ) {
1529
		$this->unsanitized_post_values(); // Populate _post_values from $_POST['customized'].
1530
		$this->_post_values[ $setting_id ] = $value;
1531
1532
		/**
1533
		 * Announce when a specific setting's unsanitized post value has been set.
1534
		 *
1535
		 * Fires when the WP_Customize_Manager::set_post_value() method is called.
1536
		 *
1537
		 * The dynamic portion of the hook name, `$setting_id`, refers to the setting ID.
1538
		 *
1539
		 * @since 4.4.0
1540
		 *
1541
		 * @param mixed                $value Unsanitized setting post value.
1542
		 * @param WP_Customize_Manager $this  WP_Customize_Manager instance.
1543
		 */
1544
		do_action( "customize_post_value_set_{$setting_id}", $value, $this );
1545
1546
		/**
1547
		 * Announce when any setting's unsanitized post value has been set.
1548
		 *
1549
		 * Fires when the WP_Customize_Manager::set_post_value() method is called.
1550
		 *
1551
		 * This is useful for `WP_Customize_Setting` instances to watch
1552
		 * in order to update a cached previewed value.
1553
		 *
1554
		 * @since 4.4.0
1555
		 *
1556
		 * @param string               $setting_id Setting ID.
1557
		 * @param mixed                $value      Unsanitized setting post value.
1558
		 * @param WP_Customize_Manager $this       WP_Customize_Manager instance.
1559
		 */
1560
		do_action( 'customize_post_value_set', $setting_id, $value, $this );
1561
	}
1562
1563
	/**
1564
	 * Print JavaScript settings.
1565
	 *
1566
	 * @since 3.4.0
1567
	 */
1568
	public function customize_preview_init() {
1569
1570
		/*
1571
		 * Now that Customizer previews are loaded into iframes via GET requests
1572
		 * and natural URLs with transaction UUIDs added, we need to ensure that
1573
		 * the responses are never cached by proxies. In practice, this will not
1574
		 * be needed if the user is logged-in anyway. But if anonymous access is
1575
		 * allowed then the auth cookies would not be sent and WordPress would
1576
		 * not send no-cache headers by default.
1577
		 */
1578
		if ( ! headers_sent() ) {
1579
			nocache_headers();
1580
			header( 'X-Robots: noindex, nofollow, noarchive' );
1581
		}
1582
		add_action( 'wp_head', 'wp_no_robots' );
1583
		add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) );
1584
1585
		/*
1586
		 * If preview is being served inside the customizer preview iframe, and
1587
		 * if the user doesn't have customize capability, then it is assumed
1588
		 * that the user's session has expired and they need to re-authenticate.
1589
		 */
1590 View Code Duplication
		if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) {
1591
			$this->wp_die( -1, __( 'Unauthorized. You may remove the customize_messenger_channel param to preview as frontend.' ) );
1592
			return;
1593
		}
1594
1595
		$this->prepare_controls();
1596
1597
		add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) );
1598
1599
		wp_enqueue_script( 'customize-preview' );
1600
		wp_enqueue_style( 'customize-preview' );
1601
		add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) );
1602
		add_action( 'wp_head', array( $this, 'remove_frameless_preview_messenger_channel' ) );
1603
		add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 );
1604
		add_filter( 'get_edit_post_link', '__return_empty_string' );
1605
1606
		/**
1607
		 * Fires once the Customizer preview has initialized and JavaScript
1608
		 * settings have been printed.
1609
		 *
1610
		 * @since 3.4.0
1611
		 *
1612
		 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
1613
		 */
1614
		do_action( 'customize_preview_init', $this );
1615
	}
1616
1617
	/**
1618
	 * Filter the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer.
1619
	 *
1620
	 * @since 4.7.0
1621
	 * @access public
1622
	 *
1623
	 * @param array $headers Headers.
1624
	 * @return array Headers.
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
1625
	 */
1626
	public function filter_iframe_security_headers( $headers ) {
1627
		$customize_url = admin_url( 'customize.php' );
1628
		$headers['X-Frame-Options'] = 'ALLOW-FROM ' . $customize_url;
1629
		$headers['Content-Security-Policy'] = 'frame-ancestors ' . preg_replace( '#^(\w+://[^/]+).+?$#', '$1', $customize_url );
1630
		return $headers;
1631
	}
1632
1633
	/**
1634
	 * Add customize state query params to a given URL if preview is allowed.
1635
	 *
1636
	 * @since 4.7.0
1637
	 * @access public
1638
	 * @see wp_redirect()
1639
	 * @see WP_Customize_Manager::get_allowed_url()
1640
	 *
1641
	 * @param string $url URL.
1642
	 * @return string URL.
1643
	 */
1644
	public function add_state_query_params( $url ) {
1645
		$parsed_original_url = wp_parse_url( $url );
1646
		$is_allowed = false;
1647
		foreach ( $this->get_allowed_urls() as $allowed_url ) {
1648
			$parsed_allowed_url = wp_parse_url( $allowed_url );
1649
			$is_allowed = (
1650
				$parsed_allowed_url['scheme'] === $parsed_original_url['scheme']
1651
				&&
1652
				$parsed_allowed_url['host'] === $parsed_original_url['host']
1653
				&&
1654
				0 === strpos( $parsed_original_url['path'], $parsed_allowed_url['path'] )
1655
			);
1656
			if ( $is_allowed ) {
1657
				break;
1658
			}
1659
		}
1660
1661
		if ( $is_allowed ) {
1662
			$query_params = array(
1663
				'customize_changeset_uuid' => $this->changeset_uuid(),
1664
			);
1665
			if ( ! $this->is_theme_active() ) {
1666
				$query_params['customize_theme'] = $this->get_stylesheet();
1667
			}
1668
			if ( $this->messenger_channel ) {
1669
				$query_params['customize_messenger_channel'] = $this->messenger_channel;
1670
			}
1671
			$url = add_query_arg( $query_params, $url );
1672
		}
1673
1674
		return $url;
1675
	}
1676
1677
	/**
1678
	 * Prevent sending a 404 status when returning the response for the customize
1679
	 * preview, since it causes the jQuery Ajax to fail. Send 200 instead.
1680
	 *
1681
	 * @since 4.0.0
1682
	 * @deprecated 4.7.0
1683
	 * @access public
1684
	 */
1685
	public function customize_preview_override_404_status() {
1686
		_deprecated_function( __METHOD__, '4.7.0' );
1687
	}
1688
1689
	/**
1690
	 * Print base element for preview frame.
1691
	 *
1692
	 * @since 3.4.0
1693
	 * @deprecated 4.7.0
1694
	 */
1695
	public function customize_preview_base() {
1696
		_deprecated_function( __METHOD__, '4.7.0' );
1697
	}
1698
1699
	/**
1700
	 * Print a workaround to handle HTML5 tags in IE < 9.
1701
	 *
1702
	 * @since 3.4.0
1703
	 * @deprecated 4.7.0 Customizer no longer supports IE8, so all supported browsers recognize HTML5.
1704
	 */
1705
	public function customize_preview_html5() {
1706
		_deprecated_function( __FUNCTION__, '4.7.0' );
1707
	}
1708
1709
	/**
1710
	 * Print CSS for loading indicators for the Customizer preview.
1711
	 *
1712
	 * @since 4.2.0
1713
	 * @access public
1714
	 */
1715
	public function customize_preview_loading_style() {
1716
		?><style>
1717
			body.wp-customizer-unloading {
1718
				opacity: 0.25;
1719
				cursor: progress !important;
1720
				-webkit-transition: opacity 0.5s;
1721
				transition: opacity 0.5s;
1722
			}
1723
			body.wp-customizer-unloading * {
1724
				pointer-events: none !important;
1725
			}
1726
			form.customize-unpreviewable,
1727
			form.customize-unpreviewable input,
1728
			form.customize-unpreviewable select,
1729
			form.customize-unpreviewable button,
1730
			a.customize-unpreviewable,
1731
			area.customize-unpreviewable {
1732
				cursor: not-allowed !important;
1733
			}
1734
		</style><?php
1735
	}
1736
1737
	/**
1738
	 * Remove customize_messenger_channel query parameter from the preview window when it is not in an iframe.
1739
	 *
1740
	 * This ensures that the admin bar will be shown. It also ensures that link navigation will
1741
	 * work as expected since the parent frame is not being sent the URL to navigate to.
1742
	 *
1743
	 * @since 4.7.0
1744
	 * @access public
1745
	 */
1746
	public function remove_frameless_preview_messenger_channel() {
1747
		if ( ! $this->messenger_channel ) {
1748
			return;
1749
		}
1750
		?>
1751
		<script>
1752
		( function() {
1753
			var urlParser, oldQueryParams, newQueryParams, i;
1754
			if ( parent !== window ) {
1755
				return;
1756
			}
1757
			urlParser = document.createElement( 'a' );
1758
			urlParser.href = location.href;
1759
			oldQueryParams = urlParser.search.substr( 1 ).split( /&/ );
1760
			newQueryParams = [];
1761
			for ( i = 0; i < oldQueryParams.length; i += 1 ) {
1762
				if ( ! /^customize_messenger_channel=/.test( oldQueryParams[ i ] ) ) {
1763
					newQueryParams.push( oldQueryParams[ i ] );
1764
				}
1765
			}
1766
			urlParser.search = newQueryParams.join( '&' );
1767
			if ( urlParser.search !== location.search ) {
1768
				location.replace( urlParser.href );
1769
			}
1770
		} )();
1771
		</script>
1772
		<?php
1773
	}
1774
1775
	/**
1776
	 * Print JavaScript settings for preview frame.
1777
	 *
1778
	 * @since 3.4.0
1779
	 */
1780
	public function customize_preview_settings() {
1781
		$post_values = $this->unsanitized_post_values( array( 'exclude_changeset' => true ) );
1782
		$setting_validities = $this->validate_setting_values( $post_values );
1783
		$exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
1784
1785
		// Note that the REQUEST_URI is not passed into home_url() since this breaks subdirectory installs.
1786
		$self_url = empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($_SERVER['REQUEST_URI']) targeting wp_unslash() can also be of type array; however, esc_url_raw() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1787
		$state_query_params = array(
1788
			'customize_theme',
1789
			'customize_changeset_uuid',
1790
			'customize_messenger_channel',
1791
		);
1792
		$self_url = remove_query_arg( $state_query_params, $self_url );
1793
1794
		$allowed_urls = $this->get_allowed_urls();
1795
		$allowed_hosts = array();
1796
		foreach ( $allowed_urls as $allowed_url ) {
1797
			$parsed = wp_parse_url( $allowed_url );
1798
			if ( empty( $parsed['host'] ) ) {
1799
				continue;
1800
			}
1801
			$host = $parsed['host'];
1802
			if ( ! empty( $parsed['port'] ) ) {
1803
				$host .= ':' . $parsed['port'];
1804
			}
1805
			$allowed_hosts[] = $host;
1806
		}
1807
1808
		$switched_locale = switch_to_locale( get_user_locale() );
1809
		$l10n = array(
1810
			'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
1811
			'linkUnpreviewable' => __( 'This link is not live-previewable.' ),
1812
			'formUnpreviewable' => __( 'This form is not live-previewable.' ),
1813
		);
1814
		if ( $switched_locale ) {
1815
			restore_previous_locale();
1816
		}
1817
1818
		$settings = array(
1819
			'changeset' => array(
1820
				'uuid' => $this->_changeset_uuid,
1821
			),
1822
			'timeouts' => array(
1823
				'selectiveRefresh' => 250,
1824
				'keepAliveSend' => 1000,
1825
			),
1826
			'theme' => array(
1827
				'stylesheet' => $this->get_stylesheet(),
1828
				'active'     => $this->is_theme_active(),
1829
			),
1830
			'url' => array(
1831
				'self' => $self_url,
1832
				'allowed' => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
1833
				'allowedHosts' => array_unique( $allowed_hosts ),
1834
				'isCrossDomain' => $this->is_cross_domain(),
1835
			),
1836
			'channel' => $this->messenger_channel,
1837
			'activePanels' => array(),
1838
			'activeSections' => array(),
1839
			'activeControls' => array(),
1840
			'settingValidities' => $exported_setting_validities,
1841
			'nonce' => current_user_can( 'customize' ) ? $this->get_nonces() : array(),
1842
			'l10n' => $l10n,
1843
			'_dirty' => array_keys( $post_values ),
1844
		);
1845
1846 View Code Duplication
		foreach ( $this->panels as $panel_id => $panel ) {
1847
			if ( $panel->check_capabilities() ) {
1848
				$settings['activePanels'][ $panel_id ] = $panel->active();
1849
				foreach ( $panel->sections as $section_id => $section ) {
1850
					if ( $section->check_capabilities() ) {
1851
						$settings['activeSections'][ $section_id ] = $section->active();
1852
					}
1853
				}
1854
			}
1855
		}
1856
		foreach ( $this->sections as $id => $section ) {
1857
			if ( $section->check_capabilities() ) {
1858
				$settings['activeSections'][ $id ] = $section->active();
1859
			}
1860
		}
1861
		foreach ( $this->controls as $id => $control ) {
1862
			if ( $control->check_capabilities() ) {
1863
				$settings['activeControls'][ $id ] = $control->active();
1864
			}
1865
		}
1866
1867
		?>
1868
		<script type="text/javascript">
1869
			var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
1870
			_wpCustomizeSettings.values = {};
1871
			(function( v ) {
1872
				<?php
1873
				/*
1874
				 * Serialize settings separately from the initial _wpCustomizeSettings
1875
				 * serialization in order to avoid a peak memory usage spike.
1876
				 * @todo We may not even need to export the values at all since the pane syncs them anyway.
1877
				 */
1878 View Code Duplication
				foreach ( $this->settings as $id => $setting ) {
1879
					if ( $setting->check_capabilities() ) {
1880
						printf(
1881
							"v[%s] = %s;\n",
1882
							wp_json_encode( $id ),
1883
							wp_json_encode( $setting->js_value() )
1884
						);
1885
					}
1886
				}
1887
				?>
1888
			})( _wpCustomizeSettings.values );
1889
		</script>
1890
		<?php
1891
	}
1892
1893
	/**
1894
	 * Prints a signature so we can ensure the Customizer was properly executed.
1895
	 *
1896
	 * @since 3.4.0
1897
	 * @deprecated 4.7.0
1898
	 */
1899
	public function customize_preview_signature() {
1900
		_deprecated_function( __METHOD__, '4.7.0' );
1901
	}
1902
1903
	/**
1904
	 * Removes the signature in case we experience a case where the Customizer was not properly executed.
1905
	 *
1906
	 * @since 3.4.0
1907
	 * @deprecated 4.7.0
1908
	 *
1909
	 * @param mixed $return Value passed through for {@see 'wp_die_handler'} filter.
1910
	 * @return mixed Value passed through for {@see 'wp_die_handler'} filter.
1911
	 */
1912
	public function remove_preview_signature( $return = null ) {
1913
		_deprecated_function( __METHOD__, '4.7.0' );
1914
1915
		return $return;
1916
	}
1917
1918
	/**
1919
	 * Is it a theme preview?
1920
	 *
1921
	 * @since 3.4.0
1922
	 *
1923
	 * @return bool True if it's a preview, false if not.
1924
	 */
1925
	public function is_preview() {
1926
		return (bool) $this->previewing;
1927
	}
1928
1929
	/**
1930
	 * Retrieve the template name of the previewed theme.
1931
	 *
1932
	 * @since 3.4.0
1933
	 *
1934
	 * @return string Template name.
1935
	 */
1936
	public function get_template() {
1937
		return $this->theme()->get_template();
1938
	}
1939
1940
	/**
1941
	 * Retrieve the stylesheet name of the previewed theme.
1942
	 *
1943
	 * @since 3.4.0
1944
	 *
1945
	 * @return string Stylesheet name.
1946
	 */
1947
	public function get_stylesheet() {
1948
		return $this->theme()->get_stylesheet();
1949
	}
1950
1951
	/**
1952
	 * Retrieve the template root of the previewed theme.
1953
	 *
1954
	 * @since 3.4.0
1955
	 *
1956
	 * @return string Theme root.
1957
	 */
1958
	public function get_template_root() {
1959
		return get_raw_theme_root( $this->get_template(), true );
1960
	}
1961
1962
	/**
1963
	 * Retrieve the stylesheet root of the previewed theme.
1964
	 *
1965
	 * @since 3.4.0
1966
	 *
1967
	 * @return string Theme root.
1968
	 */
1969
	public function get_stylesheet_root() {
1970
		return get_raw_theme_root( $this->get_stylesheet(), true );
1971
	}
1972
1973
	/**
1974
	 * Filters the current theme and return the name of the previewed theme.
1975
	 *
1976
	 * @since 3.4.0
1977
	 *
1978
	 * @param $current_theme {@internal Parameter is not used}
1979
	 * @return string Theme name.
0 ignored issues
show
Documentation introduced by
Should the return type not be false|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1980
	 */
1981
	public function current_theme( $current_theme ) {
0 ignored issues
show
Unused Code introduced by
The parameter $current_theme is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1982
		return $this->theme()->display('Name');
1983
	}
1984
1985
	/**
1986
	 * Validates setting values.
1987
	 *
1988
	 * Validation is skipped for unregistered settings or for values that are
1989
	 * already null since they will be skipped anyway. Sanitization is applied
1990
	 * to values that pass validation, and values that become null or `WP_Error`
1991
	 * after sanitizing are marked invalid.
1992
	 *
1993
	 * @since 4.6.0
1994
	 * @access public
1995
	 *
1996
	 * @see WP_REST_Request::has_valid_params()
1997
	 * @see WP_Customize_Setting::validate()
1998
	 *
1999
	 * @param array $setting_values Mapping of setting IDs to values to validate and sanitize.
2000
	 * @param array $options {
2001
	 *     Options.
2002
	 *
2003
	 *     @type bool $validate_existence  Whether a setting's existence will be checked.
2004
	 *     @type bool $validate_capability Whether the setting capability will be checked.
2005
	 * }
2006
	 * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`.
2007
	 */
2008
	public function validate_setting_values( $setting_values, $options = array() ) {
2009
		$options = wp_parse_args( $options, array(
2010
			'validate_capability' => false,
2011
			'validate_existence' => false,
2012
		) );
2013
2014
		$validities = array();
2015
		foreach ( $setting_values as $setting_id => $unsanitized_value ) {
2016
			$setting = $this->get_setting( $setting_id );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $setting is correct as $this->get_setting($setting_id) (which targets WP_Customize_Manager::get_setting()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2017
			if ( ! $setting ) {
2018
				if ( $options['validate_existence'] ) {
2019
					$validities[ $setting_id ] = new WP_Error( 'unrecognized', __( 'Setting does not exist or is unrecognized.' ) );
2020
				}
2021
				continue;
2022
			}
2023
			if ( $options['validate_capability'] && ! current_user_can( $setting->capability ) ) {
2024
				$validity = new WP_Error( 'unauthorized', __( 'Unauthorized to modify setting due to capability.' ) );
2025
			} else {
2026
				if ( is_null( $unsanitized_value ) ) {
2027
					continue;
2028
				}
2029
				$validity = $setting->validate( $unsanitized_value );
2030
			}
2031
			if ( ! is_wp_error( $validity ) ) {
2032
				/** This filter is documented in wp-includes/class-wp-customize-setting.php */
2033
				$late_validity = apply_filters( "customize_validate_{$setting->id}", new WP_Error(), $unsanitized_value, $setting );
2034
				if ( ! empty( $late_validity->errors ) ) {
2035
					$validity = $late_validity;
2036
				}
2037
			}
2038
			if ( ! is_wp_error( $validity ) ) {
2039
				$value = $setting->sanitize( $unsanitized_value );
2040
				if ( is_null( $value ) ) {
2041
					$validity = false;
2042
				} elseif ( is_wp_error( $value ) ) {
2043
					$validity = $value;
2044
				}
2045
			}
2046
			if ( false === $validity ) {
2047
				$validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) );
2048
			}
2049
			$validities[ $setting_id ] = $validity;
2050
		}
2051
		return $validities;
2052
	}
2053
2054
	/**
2055
	 * Prepares setting validity for exporting to the client (JS).
2056
	 *
2057
	 * Converts `WP_Error` instance into array suitable for passing into the
2058
	 * `wp.customize.Notification` JS model.
2059
	 *
2060
	 * @since 4.6.0
2061
	 * @access public
2062
	 *
2063
	 * @param true|WP_Error $validity Setting validity.
2064
	 * @return true|array If `$validity` was a WP_Error, the error codes will be array-mapped
0 ignored issues
show
Documentation introduced by
Should the return type not be array|boolean? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
2065
	 *                    to their respective `message` and `data` to pass into the
2066
	 *                    `wp.customize.Notification` JS model.
2067
	 */
2068
	public function prepare_setting_validity_for_js( $validity ) {
2069
		if ( is_wp_error( $validity ) ) {
2070
			$notification = array();
2071
			foreach ( $validity->errors as $error_code => $error_messages ) {
2072
				$notification[ $error_code ] = array(
2073
					'message' => join( ' ', $error_messages ),
2074
					'data' => $validity->get_error_data( $error_code ),
2075
				);
2076
			}
2077
			return $notification;
2078
		} else {
2079
			return true;
2080
		}
2081
	}
2082
2083
	/**
2084
	 * Handle customize_save WP Ajax request to save/update a changeset.
2085
	 *
2086
	 * @since 3.4.0
2087
	 * @since 4.7.0 The semantics of this method have changed to update a changeset, optionally to also change the status and other attributes.
2088
	 */
2089
	public function save() {
2090
		if ( ! is_user_logged_in() ) {
2091
			wp_send_json_error( 'unauthenticated' );
2092
		}
2093
2094
		if ( ! $this->is_preview() ) {
2095
			wp_send_json_error( 'not_preview' );
2096
		}
2097
2098
		$action = 'save-customize_' . $this->get_stylesheet();
2099
		if ( ! check_ajax_referer( $action, 'nonce', false ) ) {
2100
			wp_send_json_error( 'invalid_nonce' );
2101
		}
2102
2103
		$changeset_post_id = $this->changeset_post_id();
2104
		if ( empty( $changeset_post_id ) ) {
2105
			if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->create_posts ) ) {
2106
				wp_send_json_error( 'cannot_create_changeset_post' );
2107
			}
2108
		} else {
2109
			if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
2110
				wp_send_json_error( 'cannot_edit_changeset_post' );
2111
			}
2112
		}
2113
2114
		if ( ! empty( $_POST['customize_changeset_data'] ) ) {
2115
			$input_changeset_data = json_decode( wp_unslash( $_POST['customize_changeset_data'] ), true );
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($_POST['customize_changeset_data']) targeting wp_unslash() can also be of type array; however, json_decode() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2116
			if ( ! is_array( $input_changeset_data ) ) {
2117
				wp_send_json_error( 'invalid_customize_changeset_data' );
2118
			}
2119
		} else {
2120
			$input_changeset_data = array();
2121
		}
2122
2123
		// Validate title.
2124
		$changeset_title = null;
2125
		if ( isset( $_POST['customize_changeset_title'] ) ) {
2126
			$changeset_title = sanitize_text_field( wp_unslash( $_POST['customize_changeset_title'] ) );
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($_POST['customize_changeset_title']) targeting wp_unslash() can also be of type array; however, sanitize_text_field() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2127
		}
2128
2129
		// Validate changeset status param.
2130
		$is_publish = null;
2131
		$changeset_status = null;
2132
		if ( isset( $_POST['customize_changeset_status'] ) ) {
2133
			$changeset_status = wp_unslash( $_POST['customize_changeset_status'] );
2134
			if ( ! get_post_status_object( $changeset_status ) || ! in_array( $changeset_status, array( 'draft', 'pending', 'publish', 'future' ), true ) ) {
2135
				wp_send_json_error( 'bad_customize_changeset_status', 400 );
2136
			}
2137
			$is_publish = ( 'publish' === $changeset_status || 'future' === $changeset_status );
2138
			if ( $is_publish && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) {
2139
				wp_send_json_error( 'changeset_publish_unauthorized', 403 );
2140
			}
2141
		}
2142
2143
		/*
2144
		 * Validate changeset date param. Date is assumed to be in local time for
2145
		 * the WP if in MySQL format (YYYY-MM-DD HH:MM:SS). Otherwise, the date
2146
		 * is parsed with strtotime() so that ISO date format may be supplied
2147
		 * or a string like "+10 minutes".
2148
		 */
2149
		$changeset_date_gmt = null;
2150
		if ( isset( $_POST['customize_changeset_date'] ) ) {
2151
			$changeset_date = wp_unslash( $_POST['customize_changeset_date'] );
2152
			if ( preg_match( '/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/', $changeset_date ) ) {
2153
				$mm = substr( $changeset_date, 5, 2 );
2154
				$jj = substr( $changeset_date, 8, 2 );
2155
				$aa = substr( $changeset_date, 0, 4 );
2156
				$valid_date = wp_checkdate( $mm, $jj, $aa, $changeset_date );
2157
				if ( ! $valid_date ) {
2158
					wp_send_json_error( 'bad_customize_changeset_date', 400 );
2159
				}
2160
				$changeset_date_gmt = get_gmt_from_date( $changeset_date );
2161
			} else {
2162
				$timestamp = strtotime( $changeset_date );
2163
				if ( ! $timestamp ) {
2164
					wp_send_json_error( 'bad_customize_changeset_date', 400 );
2165
				}
2166
				$changeset_date_gmt = gmdate( 'Y-m-d H:i:s', $timestamp );
2167
			}
2168
		}
2169
2170
		$r = $this->save_changeset_post( array(
2171
			'status' => $changeset_status,
2172
			'title' => $changeset_title,
2173
			'date_gmt' => $changeset_date_gmt,
2174
			'data' => $input_changeset_data,
2175
		) );
2176
		if ( is_wp_error( $r ) ) {
2177
			$response = array(
2178
				'message' => $r->get_error_message(),
2179
				'code' => $r->get_error_code(),
2180
			);
2181
			if ( is_array( $r->get_error_data() ) ) {
2182
				$response = array_merge( $response, $r->get_error_data() );
2183
			} else {
2184
				$response['data'] = $r->get_error_data();
2185
			}
2186
		} else {
2187
			$response = $r;
2188
2189
			// Note that if the changeset status was publish, then it will get set to trash if revisions are not supported.
2190
			$response['changeset_status'] = get_post_status( $this->changeset_post_id() );
2191
			if ( $is_publish && 'trash' === $response['changeset_status'] ) {
2192
				$response['changeset_status'] = 'publish';
2193
			}
2194
2195
			if ( 'publish' === $response['changeset_status'] ) {
2196
				$response['next_changeset_uuid'] = wp_generate_uuid4();
2197
			}
2198
		}
2199
2200
		if ( isset( $response['setting_validities'] ) ) {
2201
			$response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] );
2202
		}
2203
2204
		/**
2205
		 * Filters response data for a successful customize_save Ajax request.
2206
		 *
2207
		 * This filter does not apply if there was a nonce or authentication failure.
2208
		 *
2209
		 * @since 4.2.0
2210
		 *
2211
		 * @param array                $response Additional information passed back to the 'saved'
2212
		 *                                       event on `wp.customize`.
2213
		 * @param WP_Customize_Manager $this     WP_Customize_Manager instance.
2214
		 */
2215
		$response = apply_filters( 'customize_save_response', $response, $this );
2216
2217
		if ( is_wp_error( $r ) ) {
2218
			wp_send_json_error( $response );
2219
		} else {
2220
			wp_send_json_success( $response );
2221
		}
2222
	}
2223
2224
	/**
2225
	 * Save the post for the loaded changeset.
2226
	 *
2227
	 * @since 4.7.0
2228
	 * @access public
2229
	 *
2230
	 * @param array $args {
2231
	 *     Args for changeset post.
2232
	 *
2233
	 *     @type array  $data            Optional additional changeset data. Values will be merged on top of any existing post values.
2234
	 *     @type string $status          Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed.
2235
	 *     @type string $title           Post title. Optional.
2236
	 *     @type string $date_gmt        Date in GMT. Optional.
2237
	 *     @type int    $user_id         ID for user who is saving the changeset. Optional, defaults to the current user ID.
2238
	 *     @type bool   $starter_content Whether the data is starter content. If false (default), then $starter_content will be cleared for any $data being saved.
2239
	 * }
2240
	 *
2241
	 * @return array|WP_Error Returns array on success and WP_Error with array data on error.
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use WP_Error|array<string,array>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
2242
	 */
2243
	function save_changeset_post( $args = array() ) {
2244
2245
		$args = array_merge(
2246
			array(
2247
				'status' => null,
2248
				'title' => null,
2249
				'data' => array(),
2250
				'date_gmt' => null,
2251
				'user_id' => get_current_user_id(),
2252
				'starter_content' => false,
2253
			),
2254
			$args
2255
		);
2256
2257
		$changeset_post_id = $this->changeset_post_id();
2258
		$existing_changeset_data = array();
2259
		if ( $changeset_post_id ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $changeset_post_id of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2260
			$existing_status = get_post_status( $changeset_post_id );
2261
			if ( 'publish' === $existing_status || 'trash' === $existing_status ) {
2262
				return new WP_Error( 'changeset_already_published' );
2263
			}
2264
2265
			$existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2266
		}
2267
2268
		// Fail if attempting to publish but publish hook is missing.
2269
		if ( 'publish' === $args['status'] && false === has_action( 'transition_post_status', '_wp_customize_publish_changeset' ) ) {
2270
			return new WP_Error( 'missing_publish_callback' );
2271
		}
2272
2273
		// Validate date.
2274
		$now = gmdate( 'Y-m-d H:i:59' );
2275
		if ( $args['date_gmt'] ) {
2276
			$is_future_dated = ( mysql2date( 'U', $args['date_gmt'], false ) > mysql2date( 'U', $now, false ) );
2277
			if ( ! $is_future_dated ) {
2278
				return new WP_Error( 'not_future_date' ); // Only future dates are allowed.
2279
			}
2280
2281
			if ( ! $this->is_theme_active() && ( 'future' === $args['status'] || $is_future_dated ) ) {
2282
				return new WP_Error( 'cannot_schedule_theme_switches' ); // This should be allowed in the future, when theme is a regular setting.
2283
			}
2284
			$will_remain_auto_draft = ( ! $args['status'] && ( ! $changeset_post_id || 'auto-draft' === get_post_status( $changeset_post_id ) ) );
0 ignored issues
show
Bug Best Practice introduced by
The expression $changeset_post_id of type null|integer is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2285
			if ( $will_remain_auto_draft ) {
2286
				return new WP_Error( 'cannot_supply_date_for_auto_draft_changeset' );
2287
			}
2288
		} elseif ( $changeset_post_id && 'future' === $args['status'] ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $changeset_post_id of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2289
2290
			// Fail if the new status is future but the existing post's date is not in the future.
2291
			$changeset_post = get_post( $changeset_post_id );
2292
			if ( mysql2date( 'U', $changeset_post->post_date_gmt, false ) <= mysql2date( 'U', $now, false ) ) {
2293
				return new WP_Error( 'not_future_date' );
2294
			}
2295
		}
2296
2297
		// The request was made via wp.customize.previewer.save().
2298
		$update_transactionally = (bool) $args['status'];
2299
		$allow_revision = (bool) $args['status'];
2300
2301
		// Amend post values with any supplied data.
2302
		foreach ( $args['data'] as $setting_id => $setting_params ) {
2303
			if ( array_key_exists( 'value', $setting_params ) ) {
2304
				$this->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized.
2305
			}
2306
		}
2307
2308
		// Note that in addition to post data, this will include any stashed theme mods.
2309
		$post_values = $this->unsanitized_post_values( array(
2310
			'exclude_changeset' => true,
2311
			'exclude_post_data' => false,
2312
		) );
2313
		$this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
2314
2315
		/*
2316
		 * Get list of IDs for settings that have values different from what is currently
2317
		 * saved in the changeset. By skipping any values that are already the same, the
2318
		 * subset of changed settings can be passed into validate_setting_values to prevent
2319
		 * an underprivileged modifying a single setting for which they have the capability
2320
		 * from being blocked from saving. This also prevents a user from touching of the
2321
		 * previous saved settings and overriding the associated user_id if they made no change.
2322
		 */
2323
		$changed_setting_ids = array();
2324
		foreach ( $post_values as $setting_id => $setting_value ) {
2325
			$setting = $this->get_setting( $setting_id );
2326
2327
			if ( $setting && 'theme_mod' === $setting->type ) {
2328
				$prefixed_setting_id = $this->get_stylesheet() . '::' . $setting->id;
2329
			} else {
2330
				$prefixed_setting_id = $setting_id;
2331
			}
2332
2333
			$is_value_changed = (
2334
				! isset( $existing_changeset_data[ $prefixed_setting_id ] )
2335
				||
2336
				! array_key_exists( 'value', $existing_changeset_data[ $prefixed_setting_id ] )
2337
				||
2338
				$existing_changeset_data[ $prefixed_setting_id ]['value'] !== $setting_value
2339
			);
2340
			if ( $is_value_changed ) {
2341
				$changed_setting_ids[] = $setting_id;
2342
			}
2343
		}
2344
2345
		/**
2346
		 * Fires before save validation happens.
2347
		 *
2348
		 * Plugins can add just-in-time {@see 'customize_validate_{$this->ID}'} filters
2349
		 * at this point to catch any settings registered after `customize_register`.
2350
		 * The dynamic portion of the hook name, `$this->ID` refers to the setting ID.
2351
		 *
2352
		 * @since 4.6.0
2353
		 *
2354
		 * @param WP_Customize_Manager $this WP_Customize_Manager instance.
2355
		 */
2356
		do_action( 'customize_save_validation_before', $this );
2357
2358
		// Validate settings.
2359
		$validated_values = array_merge(
2360
			array_fill_keys( array_keys( $args['data'] ), null ), // Make sure existence/capability checks are done on value-less setting updates.
2361
			$post_values
2362
		);
2363
		$setting_validities = $this->validate_setting_values( $validated_values, array(
2364
			'validate_capability' => true,
2365
			'validate_existence' => true,
2366
		) );
2367
		$invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) );
2368
2369
		/*
2370
		 * Short-circuit if there are invalid settings the update is transactional.
2371
		 * A changeset update is transactional when a status is supplied in the request.
2372
		 */
2373
		if ( $update_transactionally && $invalid_setting_count > 0 ) {
2374
			$response = array(
2375
				'setting_validities' => $setting_validities,
2376
				'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
2377
			);
2378
			return new WP_Error( 'transaction_fail', '', $response );
2379
		}
2380
2381
		// Obtain/merge data for changeset.
2382
		$original_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2383
		$data = $original_changeset_data;
2384
		if ( is_wp_error( $data ) ) {
2385
			$data = array();
2386
		}
2387
2388
		// Ensure that all post values are included in the changeset data.
2389
		foreach ( $post_values as $setting_id => $post_value ) {
2390
			if ( ! isset( $args['data'][ $setting_id ] ) ) {
2391
				$args['data'][ $setting_id ] = array();
2392
			}
2393
			if ( ! isset( $args['data'][ $setting_id ]['value'] ) ) {
2394
				$args['data'][ $setting_id ]['value'] = $post_value;
2395
			}
2396
		}
2397
2398
		foreach ( $args['data'] as $setting_id => $setting_params ) {
2399
			$setting = $this->get_setting( $setting_id );
2400
			if ( ! $setting || ! $setting->check_capabilities() ) {
2401
				continue;
2402
			}
2403
2404
			// Skip updating changeset for invalid setting values.
2405
			if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) {
2406
				continue;
2407
			}
2408
2409
			$changeset_setting_id = $setting_id;
2410
			if ( 'theme_mod' === $setting->type ) {
2411
				$changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id );
2412
			}
2413
2414
			if ( null === $setting_params ) {
2415
				// Remove setting from changeset entirely.
2416
				unset( $data[ $changeset_setting_id ] );
2417
			} else {
2418
2419
				if ( ! isset( $data[ $changeset_setting_id ] ) ) {
2420
					$data[ $changeset_setting_id ] = array();
2421
				}
2422
2423
				// Merge any additional setting params that have been supplied with the existing params.
2424
				$merged_setting_params = array_merge( $data[ $changeset_setting_id ], $setting_params );
2425
2426
				// Skip updating setting params if unchanged (ensuring the user_id is not overwritten).
2427
				if ( $data[ $changeset_setting_id ] === $merged_setting_params ) {
2428
					continue;
2429
				}
2430
2431
				$data[ $changeset_setting_id ] = array_merge(
2432
					$merged_setting_params,
2433
					array(
2434
						'type' => $setting->type,
2435
						'user_id' => $args['user_id'],
2436
					)
2437
				);
2438
2439
				// Clear starter_content flag in data if changeset is not explicitly being updated for starter content.
2440
				if ( empty( $args['starter_content'] ) ) {
2441
					unset( $data[ $changeset_setting_id ]['starter_content'] );
2442
				}
2443
			}
2444
		}
2445
2446
		$filter_context = array(
2447
			'uuid' => $this->changeset_uuid(),
2448
			'title' => $args['title'],
2449
			'status' => $args['status'],
2450
			'date_gmt' => $args['date_gmt'],
2451
			'post_id' => $changeset_post_id,
2452
			'previous_data' => is_wp_error( $original_changeset_data ) ? array() : $original_changeset_data,
2453
			'manager' => $this,
2454
		);
2455
2456
		/**
2457
		 * Filters the settings' data that will be persisted into the changeset.
2458
		 *
2459
		 * Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter.
2460
		 *
2461
		 * @since 4.7.0
2462
		 *
2463
		 * @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata.
2464
		 * @param array $context {
2465
		 *     Filter context.
2466
		 *
2467
		 *     @type string               $uuid          Changeset UUID.
2468
		 *     @type string               $title         Requested title for the changeset post.
2469
		 *     @type string               $status        Requested status for the changeset post.
2470
		 *     @type string               $date_gmt      Requested date for the changeset post in MySQL format and GMT timezone.
2471
		 *     @type int|false            $post_id       Post ID for the changeset, or false if it doesn't exist yet.
2472
		 *     @type array                $previous_data Previous data contained in the changeset.
2473
		 *     @type WP_Customize_Manager $manager       Manager instance.
2474
		 * }
2475
		 */
2476
		$data = apply_filters( 'customize_changeset_save_data', $data, $filter_context );
2477
2478
		// Switch theme if publishing changes now.
2479
		if ( 'publish' === $args['status'] && ! $this->is_theme_active() ) {
2480
			// Temporarily stop previewing the theme to allow switch_themes() to operate properly.
2481
			$this->stop_previewing_theme();
2482
			switch_theme( $this->get_stylesheet() );
2483
			update_option( 'theme_switched_via_customizer', true );
2484
			$this->start_previewing_theme();
2485
		}
2486
2487
		// Gather the data for wp_insert_post()/wp_update_post().
2488
		$json_options = 0;
2489
		if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
2490
			$json_options |= JSON_UNESCAPED_SLASHES; // Introduced in PHP 5.4. This is only to improve readability as slashes needn't be escaped in storage.
2491
		}
2492
		$json_options |= JSON_PRETTY_PRINT; // Also introduced in PHP 5.4, but WP defines constant for back compat. See WP Trac #30139.
2493
		$post_array = array(
2494
			'post_content' => wp_json_encode( $data, $json_options ),
2495
		);
2496
		if ( $args['title'] ) {
2497
			$post_array['post_title'] = $args['title'];
2498
		}
2499
		if ( $changeset_post_id ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $changeset_post_id of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2500
			$post_array['ID'] = $changeset_post_id;
2501
		} else {
2502
			$post_array['post_type'] = 'customize_changeset';
2503
			$post_array['post_name'] = $this->changeset_uuid();
2504
			$post_array['post_status'] = 'auto-draft';
2505
		}
2506
		if ( $args['status'] ) {
2507
			$post_array['post_status'] = $args['status'];
2508
		}
2509
2510
		// Reset post date to now if we are publishing, otherwise pass post_date_gmt and translate for post_date.
2511
		if ( 'publish' === $args['status'] ) {
2512
			$post_array['post_date_gmt'] = '0000-00-00 00:00:00';
2513
			$post_array['post_date'] = '0000-00-00 00:00:00';
2514
		} elseif ( $args['date_gmt'] ) {
2515
			$post_array['post_date_gmt'] = $args['date_gmt'];
2516
			$post_array['post_date'] = get_date_from_gmt( $args['date_gmt'] );
2517
		} elseif ( $changeset_post_id && 'auto-draft' === get_post_status( $changeset_post_id ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $changeset_post_id of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2518
			/*
2519
			 * Keep bumping the date for the auto-draft whenever it is modified;
2520
			 * this extends its life, preserving it from garbage-collection via
2521
			 * wp_delete_auto_drafts().
2522
			 */
2523
			$post_array['post_date'] = current_time( 'mysql' );
2524
			$post_array['post_date_gmt'] = '';
2525
		}
2526
2527
		$this->store_changeset_revision = $allow_revision;
2528
		add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 );
2529
2530
		// Update the changeset post. The publish_customize_changeset action will cause the settings in the changeset to be saved via WP_Customize_Setting::save().
2531
		$has_kses = ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) );
2532
		if ( $has_kses ) {
2533
			kses_remove_filters(); // Prevent KSES from corrupting JSON in post_content.
2534
		}
2535
2536
		// Note that updating a post with publish status will trigger WP_Customize_Manager::publish_changeset_values().
2537
		if ( $changeset_post_id ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $changeset_post_id of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2538
			$post_array['edit_date'] = true; // Prevent date clearing.
2539
			$r = wp_update_post( wp_slash( $post_array ), true );
0 ignored issues
show
Bug introduced by
It seems like wp_slash($post_array) targeting wp_slash() can also be of type string; however, wp_update_post() does only seem to accept array|object, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2540
		} else {
2541
			$r = wp_insert_post( wp_slash( $post_array ), true );
0 ignored issues
show
Bug introduced by
It seems like wp_slash($post_array) targeting wp_slash() can also be of type string; however, wp_insert_post() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2542
			if ( ! is_wp_error( $r ) ) {
2543
				$this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset.
0 ignored issues
show
Documentation Bug introduced by
It seems like $r can also be of type object<WP_Error>. However, the property $_changeset_post_id is declared as type integer|false. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2544
			}
2545
		}
2546
		if ( $has_kses ) {
2547
			kses_init_filters();
2548
		}
2549
		$this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents.
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $_changeset_data.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2550
2551
		remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) );
2552
2553
		$response = array(
2554
			'setting_validities' => $setting_validities,
2555
		);
2556
2557
		if ( is_wp_error( $r ) ) {
2558
			$response['changeset_post_save_failure'] = $r->get_error_code();
2559
			return new WP_Error( 'changeset_post_save_failure', '', $response );
2560
		}
2561
2562
		return $response;
2563
	}
2564
2565
	/**
2566
	 * Whether a changeset revision should be made.
2567
	 *
2568
	 * @since 4.7.0
2569
	 * @access private
2570
	 * @var bool
2571
	 */
2572
	protected $store_changeset_revision;
2573
2574
	/**
2575
	 * Filters whether a changeset has changed to create a new revision.
2576
	 *
2577
	 * Note that this will not be called while a changeset post remains in auto-draft status.
2578
	 *
2579
	 * @since 4.7.0
2580
	 * @access private
2581
	 *
2582
	 * @param bool    $post_has_changed Whether the post has changed.
2583
	 * @param WP_Post $last_revision    The last revision post object.
2584
	 * @param WP_Post $post             The post object.
2585
	 *
2586
	 * @return bool Whether a revision should be made.
2587
	 */
2588
	public function _filter_revision_post_has_changed( $post_has_changed, $last_revision, $post ) {
2589
		unset( $last_revision );
2590
		if ( 'customize_changeset' === $post->post_type ) {
2591
			$post_has_changed = $this->store_changeset_revision;
2592
		}
2593
		return $post_has_changed;
2594
	}
2595
2596
	/**
2597
	 * Publish changeset values.
2598
	 *
2599
	 * This will the values contained in a changeset, even changesets that do not
2600
	 * correspond to current manager instance. This is called by
2601
	 * `_wp_customize_publish_changeset()` when a customize_changeset post is
2602
	 * transitioned to the `publish` status. As such, this method should not be
2603
	 * called directly and instead `wp_publish_post()` should be used.
2604
	 *
2605
	 * Please note that if the settings in the changeset are for a non-activated
2606
	 * theme, the theme must first be switched to (via `switch_theme()`) before
2607
	 * invoking this method.
2608
	 *
2609
	 * @since 4.7.0
2610
	 * @access private
2611
	 * @see _wp_customize_publish_changeset()
2612
	 *
2613
	 * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance.
2614
	 * @return true|WP_Error True or error info.
0 ignored issues
show
Documentation introduced by
Should the return type not be WP_Error|array|boolean? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
2615
	 */
2616
	public function _publish_changeset_values( $changeset_post_id ) {
2617
		$publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2618
		if ( is_wp_error( $publishing_changeset_data ) ) {
2619
			return $publishing_changeset_data;
2620
		}
2621
2622
		$changeset_post = get_post( $changeset_post_id );
2623
2624
		/*
2625
		 * Temporarily override the changeset context so that it will be read
2626
		 * in calls to unsanitized_post_values() and so that it will be available
2627
		 * on the $wp_customize object passed to hooks during the save logic.
2628
		 */
2629
		$previous_changeset_post_id = $this->_changeset_post_id;
2630
		$this->_changeset_post_id   = $changeset_post_id;
2631
		$previous_changeset_uuid    = $this->_changeset_uuid;
2632
		$this->_changeset_uuid      = $changeset_post->post_name;
2633
		$previous_changeset_data    = $this->_changeset_data;
2634
		$this->_changeset_data      = $publishing_changeset_data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $publishing_changeset_data can also be of type object<WP_Error>. However, the property $_changeset_data is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2635
2636
		// Parse changeset data to identify theme mod settings and user IDs associated with settings to be saved.
2637
		$setting_user_ids = array();
2638
		$theme_mod_settings = array();
2639
		$namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
2640
		$matches = array();
2641
		foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
0 ignored issues
show
Bug introduced by
The expression $this->_changeset_data of type object<WP_Error>|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
2642
			$actual_setting_id = null;
2643
			$is_theme_mod_setting = (
2644
				isset( $setting_params['value'] )
2645
				&&
2646
				isset( $setting_params['type'] )
2647
				&&
2648
				'theme_mod' === $setting_params['type']
2649
				&&
2650
				preg_match( $namespace_pattern, $raw_setting_id, $matches )
2651
			);
2652
			if ( $is_theme_mod_setting ) {
2653
				if ( ! isset( $theme_mod_settings[ $matches['stylesheet'] ] ) ) {
2654
					$theme_mod_settings[ $matches['stylesheet'] ] = array();
2655
				}
2656
				$theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
2657
2658
				if ( $this->get_stylesheet() === $matches['stylesheet'] ) {
2659
					$actual_setting_id = $matches['setting_id'];
2660
				}
2661
			} else {
2662
				$actual_setting_id = $raw_setting_id;
2663
			}
2664
2665
			// Keep track of the user IDs for settings actually for this theme.
2666
			if ( $actual_setting_id && isset( $setting_params['user_id'] ) ) {
2667
				$setting_user_ids[ $actual_setting_id ] = $setting_params['user_id'];
2668
			}
2669
		}
2670
2671
		$changeset_setting_values = $this->unsanitized_post_values( array(
2672
			'exclude_post_data' => true,
2673
			'exclude_changeset' => false,
2674
		) );
2675
		$changeset_setting_ids = array_keys( $changeset_setting_values );
2676
		$this->add_dynamic_settings( $changeset_setting_ids );
2677
2678
		/**
2679
		 * Fires once the theme has switched in the Customizer, but before settings
2680
		 * have been saved.
2681
		 *
2682
		 * @since 3.4.0
2683
		 *
2684
		 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
2685
		 */
2686
		do_action( 'customize_save', $this );
2687
2688
		/*
2689
		 * Ensure that all settings will allow themselves to be saved. Note that
2690
		 * this is safe because the setting would have checked the capability
2691
		 * when the setting value was written into the changeset. So this is why
2692
		 * an additional capability check is not required here.
2693
		 */
2694
		$original_setting_capabilities = array();
2695
		foreach ( $changeset_setting_ids as $setting_id ) {
2696
			$setting = $this->get_setting( $setting_id );
2697
			if ( $setting && ! isset( $setting_user_ids[ $setting_id ] ) ) {
2698
				$original_setting_capabilities[ $setting->id ] = $setting->capability;
2699
				$setting->capability = 'exist';
2700
			}
2701
		}
2702
2703
		$original_user_id = get_current_user_id();
2704
		foreach ( $changeset_setting_ids as $setting_id ) {
2705
			$setting = $this->get_setting( $setting_id );
2706
			if ( $setting ) {
2707
				/*
2708
				 * Set the current user to match the user who saved the value into
2709
				 * the changeset so that any filters that apply during the save
2710
				 * process will respect the original user's capabilities. This
2711
				 * will ensure, for example, that KSES won't strip unsafe HTML
2712
				 * when a scheduled changeset publishes via WP Cron.
2713
				 */
2714
				if ( isset( $setting_user_ids[ $setting_id ] ) ) {
2715
					wp_set_current_user( $setting_user_ids[ $setting_id ] );
2716
				} else {
2717
					wp_set_current_user( $original_user_id );
2718
				}
2719
2720
				$setting->save();
2721
			}
2722
		}
2723
		wp_set_current_user( $original_user_id );
2724
2725
		// Update the stashed theme mod settings, removing the active theme's stashed settings, if activated.
2726
		if ( did_action( 'switch_theme' ) ) {
2727
			$other_theme_mod_settings = $theme_mod_settings;
2728
			unset( $other_theme_mod_settings[ $this->get_stylesheet() ] );
2729
			$this->update_stashed_theme_mod_settings( $other_theme_mod_settings );
2730
		}
2731
2732
		/**
2733
		 * Fires after Customize settings have been saved.
2734
		 *
2735
		 * @since 3.6.0
2736
		 *
2737
		 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
2738
		 */
2739
		do_action( 'customize_save_after', $this );
2740
2741
		// Restore original capabilities.
2742
		foreach ( $original_setting_capabilities as $setting_id => $capability ) {
2743
			$setting = $this->get_setting( $setting_id );
2744
			if ( $setting ) {
2745
				$setting->capability = $capability;
2746
			}
2747
		}
2748
2749
		// Restore original changeset data.
2750
		$this->_changeset_data    = $previous_changeset_data;
2751
		$this->_changeset_post_id = $previous_changeset_post_id;
2752
		$this->_changeset_uuid    = $previous_changeset_uuid;
2753
2754
		return true;
2755
	}
2756
2757
	/**
2758
	 * Update stashed theme mod settings.
2759
	 *
2760
	 * @since 4.7.0
2761
	 * @access private
2762
	 *
2763
	 * @param array $inactive_theme_mod_settings Mapping of stylesheet to arrays of theme mod settings.
2764
	 * @return array|false Returns array of updated stashed theme mods or false if the update failed or there were no changes.
2765
	 */
2766
	protected function update_stashed_theme_mod_settings( $inactive_theme_mod_settings ) {
2767
		$stashed_theme_mod_settings = get_option( 'customize_stashed_theme_mods' );
2768
		if ( empty( $stashed_theme_mod_settings ) ) {
2769
			$stashed_theme_mod_settings = array();
2770
		}
2771
2772
		// Delete any stashed theme mods for the active theme since since they would have been loaded and saved upon activation.
2773
		unset( $stashed_theme_mod_settings[ $this->get_stylesheet() ] );
2774
2775
		// Merge inactive theme mods with the stashed theme mod settings.
2776
		foreach ( $inactive_theme_mod_settings as $stylesheet => $theme_mod_settings ) {
2777
			if ( ! isset( $stashed_theme_mod_settings[ $stylesheet ] ) ) {
2778
				$stashed_theme_mod_settings[ $stylesheet ] = array();
2779
			}
2780
2781
			$stashed_theme_mod_settings[ $stylesheet ] = array_merge(
2782
				$stashed_theme_mod_settings[ $stylesheet ],
2783
				$theme_mod_settings
2784
			);
2785
		}
2786
2787
		$autoload = false;
2788
		$result = update_option( 'customize_stashed_theme_mods', $stashed_theme_mod_settings, $autoload );
2789
		if ( ! $result ) {
2790
			return false;
2791
		}
2792
		return $stashed_theme_mod_settings;
2793
	}
2794
2795
	/**
2796
	 * Refresh nonces for the current preview.
2797
	 *
2798
	 * @since 4.2.0
2799
	 */
2800
	public function refresh_nonces() {
2801
		if ( ! $this->is_preview() ) {
2802
			wp_send_json_error( 'not_preview' );
2803
		}
2804
2805
		wp_send_json_success( $this->get_nonces() );
2806
	}
2807
2808
	/**
2809
	 * Add a customize setting.
2810
	 *
2811
	 * @since 3.4.0
2812
	 * @since 4.5.0 Return added WP_Customize_Setting instance.
2813
	 *
2814
	 * @param WP_Customize_Setting|string $id   Customize Setting object, or ID.
2815
	 * @param array                       $args {
2816
	 *  Optional. Array of properties for the new WP_Customize_Setting. Default empty array.
2817
	 *
2818
	 *  @type string       $type                  Type of the setting. Default 'theme_mod'.
2819
	 *                                            Default 160.
2820
	 *  @type string       $capability            Capability required for the setting. Default 'edit_theme_options'
2821
	 *  @type string|array $theme_supports        Theme features required to support the panel. Default is none.
2822
	 *  @type string       $default               Default value for the setting. Default is empty string.
2823
	 *  @type string       $transport             Options for rendering the live preview of changes in Theme Customizer.
2824
	 *                                            Using 'refresh' makes the change visible by reloading the whole preview.
2825
	 *                                            Using 'postMessage' allows a custom JavaScript to handle live changes.
2826
	 *                                            @link https://developer.wordpress.org/themes/customize-api
2827
	 *                                            Default is 'refresh'
2828
	 *  @type callable     $validate_callback     Server-side validation callback for the setting's value.
2829
	 *  @type callable     $sanitize_callback     Callback to filter a Customize setting value in un-slashed form.
2830
	 *  @type callable     $sanitize_js_callback  Callback to convert a Customize PHP setting value to a value that is
2831
	 *                                            JSON serializable.
2832
	 *  @type bool         $dirty                 Whether or not the setting is initially dirty when created.
2833
	 * }
2834
	 * @return WP_Customize_Setting             The instance of the setting that was added.
2835
	 */
2836
	public function add_setting( $id, $args = array() ) {
2837
		if ( $id instanceof WP_Customize_Setting ) {
2838
			$setting = $id;
2839
		} else {
2840
			$class = 'WP_Customize_Setting';
2841
2842
			/** This filter is documented in wp-includes/class-wp-customize-manager.php */
2843
			$args = apply_filters( 'customize_dynamic_setting_args', $args, $id );
2844
2845
			/** This filter is documented in wp-includes/class-wp-customize-manager.php */
2846
			$class = apply_filters( 'customize_dynamic_setting_class', $class, $id, $args );
2847
2848
			$setting = new $class( $this, $id, $args );
2849
		}
2850
2851
		$this->settings[ $setting->id ] = $setting;
2852
		return $setting;
2853
	}
2854
2855
	/**
2856
	 * Register any dynamically-created settings, such as those from $_POST['customized']
2857
	 * that have no corresponding setting created.
2858
	 *
2859
	 * This is a mechanism to "wake up" settings that have been dynamically created
2860
	 * on the front end and have been sent to WordPress in `$_POST['customized']`. When WP
2861
	 * loads, the dynamically-created settings then will get created and previewed
2862
	 * even though they are not directly created statically with code.
2863
	 *
2864
	 * @since 4.2.0
2865
	 * @access public
2866
	 *
2867
	 * @param array $setting_ids The setting IDs to add.
2868
	 * @return array The WP_Customize_Setting objects added.
2869
	 */
2870
	public function add_dynamic_settings( $setting_ids ) {
2871
		$new_settings = array();
2872
		foreach ( $setting_ids as $setting_id ) {
2873
			// Skip settings already created
2874
			if ( $this->get_setting( $setting_id ) ) {
2875
				continue;
2876
			}
2877
2878
			$setting_args = false;
2879
			$setting_class = 'WP_Customize_Setting';
2880
2881
			/**
2882
			 * Filters a dynamic setting's constructor args.
2883
			 *
2884
			 * For a dynamic setting to be registered, this filter must be employed
2885
			 * to override the default false value with an array of args to pass to
2886
			 * the WP_Customize_Setting constructor.
2887
			 *
2888
			 * @since 4.2.0
2889
			 *
2890
			 * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
2891
			 * @param string      $setting_id   ID for dynamic setting, usually coming from `$_POST['customized']`.
2892
			 */
2893
			$setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id );
2894
			if ( false === $setting_args ) {
2895
				continue;
2896
			}
2897
2898
			/**
2899
			 * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
2900
			 *
2901
			 * @since 4.2.0
2902
			 *
2903
			 * @param string $setting_class WP_Customize_Setting or a subclass.
2904
			 * @param string $setting_id    ID for dynamic setting, usually coming from `$_POST['customized']`.
2905
			 * @param array  $setting_args  WP_Customize_Setting or a subclass.
2906
			 */
2907
			$setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args );
2908
2909
			$setting = new $setting_class( $this, $setting_id, $setting_args );
2910
2911
			$this->add_setting( $setting );
2912
			$new_settings[] = $setting;
2913
		}
2914
		return $new_settings;
2915
	}
2916
2917
	/**
2918
	 * Retrieve a customize setting.
2919
	 *
2920
	 * @since 3.4.0
2921
	 *
2922
	 * @param string $id Customize Setting ID.
2923
	 * @return WP_Customize_Setting|void The setting, if set.
2924
	 */
2925
	public function get_setting( $id ) {
2926
		if ( isset( $this->settings[ $id ] ) ) {
2927
			return $this->settings[ $id ];
2928
		}
2929
	}
2930
2931
	/**
2932
	 * Remove a customize setting.
2933
	 *
2934
	 * @since 3.4.0
2935
	 *
2936
	 * @param string $id Customize Setting ID.
2937
	 */
2938
	public function remove_setting( $id ) {
2939
		unset( $this->settings[ $id ] );
2940
	}
2941
2942
	/**
2943
	 * Add a customize panel.
2944
	 *
2945
	 * @since 4.0.0
2946
	 * @since 4.5.0 Return added WP_Customize_Panel instance.
2947
	 *
2948
	 * @param WP_Customize_Panel|string $id   Customize Panel object, or Panel ID.
2949
	 * @param array                     $args {
2950
	 *  Optional. Array of properties for the new Panel object. Default empty array.
2951
	 *  @type int          $priority              Priority of the panel, defining the display order of panels and sections.
2952
	 *                                            Default 160.
2953
	 *  @type string       $capability            Capability required for the panel. Default `edit_theme_options`
2954
	 *  @type string|array $theme_supports        Theme features required to support the panel.
2955
	 *  @type string       $title                 Title of the panel to show in UI.
2956
	 *  @type string       $description           Description to show in the UI.
2957
	 *  @type string       $type                  Type of the panel.
2958
	 *  @type callable     $active_callback       Active callback.
2959
	 * }
2960
	 * @return WP_Customize_Panel             The instance of the panel that was added.
2961
	 */
2962
	public function add_panel( $id, $args = array() ) {
2963
		if ( $id instanceof WP_Customize_Panel ) {
2964
			$panel = $id;
2965
		} else {
2966
			$panel = new WP_Customize_Panel( $this, $id, $args );
2967
		}
2968
2969
		$this->panels[ $panel->id ] = $panel;
2970
		return $panel;
2971
	}
2972
2973
	/**
2974
	 * Retrieve a customize panel.
2975
	 *
2976
	 * @since 4.0.0
2977
	 * @access public
2978
	 *
2979
	 * @param string $id Panel ID to get.
2980
	 * @return WP_Customize_Panel|void Requested panel instance, if set.
2981
	 */
2982
	public function get_panel( $id ) {
2983
		if ( isset( $this->panels[ $id ] ) ) {
2984
			return $this->panels[ $id ];
2985
		}
2986
	}
2987
2988
	/**
2989
	 * Remove a customize panel.
2990
	 *
2991
	 * @since 4.0.0
2992
	 * @access public
2993
	 *
2994
	 * @param string $id Panel ID to remove.
2995
	 */
2996
	public function remove_panel( $id ) {
2997
		// Removing core components this way is _doing_it_wrong().
2998
		if ( in_array( $id, $this->components, true ) ) {
2999
			/* translators: 1: panel id, 2: link to 'customize_loaded_components' filter reference */
3000
			$message = sprintf( __( 'Removing %1$s manually will cause PHP warnings. Use the %2$s filter instead.' ),
3001
				$id,
3002
				'<a href="' . esc_url( 'https://developer.wordpress.org/reference/hooks/customize_loaded_components/' ) . '"><code>customize_loaded_components</code></a>'
3003
			);
3004
3005
			_doing_it_wrong( __METHOD__, $message, '4.5.0' );
3006
		}
3007
		unset( $this->panels[ $id ] );
3008
	}
3009
3010
	/**
3011
	 * Register a customize panel type.
3012
	 *
3013
	 * Registered types are eligible to be rendered via JS and created dynamically.
3014
	 *
3015
	 * @since 4.3.0
3016
	 * @access public
3017
	 *
3018
	 * @see WP_Customize_Panel
3019
	 *
3020
	 * @param string $panel Name of a custom panel which is a subclass of WP_Customize_Panel.
3021
	 */
3022
	public function register_panel_type( $panel ) {
3023
		$this->registered_panel_types[] = $panel;
3024
	}
3025
3026
	/**
3027
	 * Render JS templates for all registered panel types.
3028
	 *
3029
	 * @since 4.3.0
3030
	 * @access public
3031
	 */
3032
	public function render_panel_templates() {
3033
		foreach ( $this->registered_panel_types as $panel_type ) {
3034
			$panel = new $panel_type( $this, 'temp', array() );
3035
			$panel->print_template();
3036
		}
3037
	}
3038
3039
	/**
3040
	 * Add a customize section.
3041
	 *
3042
	 * @since 3.4.0
3043
	 * @since 4.5.0 Return added WP_Customize_Section instance.
3044
	 * @access public
3045
	 *
3046
	 * @param WP_Customize_Section|string $id   Customize Section object, or Section ID.
3047
	 * @param array                     $args {
3048
	 *  Optional. Array of properties for the new Panel object. Default empty array.
3049
	 *  @type int          $priority              Priority of the panel, defining the display order of panels and sections.
3050
	 *                                            Default 160.
3051
	 *  @type string       $panel                 Priority of the panel, defining the display order of panels and sections.
3052
	 *  @type string       $capability            Capability required for the panel. Default 'edit_theme_options'
3053
	 *  @type string|array $theme_supports        Theme features required to support the panel.
3054
	 *  @type string       $title                 Title of the panel to show in UI.
3055
	 *  @type string       $description           Description to show in the UI.
3056
	 *  @type string       $type                  Type of the panel.
3057
	 *  @type callable     $active_callback       Active callback.
3058
	 *  @type bool         $description_hidden    Hide the description behind a help icon, instead of . Default false.
3059
	 * }
3060
	 * @return WP_Customize_Section             The instance of the section that was added.
3061
	 */
3062
	public function add_section( $id, $args = array() ) {
3063
		if ( $id instanceof WP_Customize_Section ) {
3064
			$section = $id;
3065
		} else {
3066
			$section = new WP_Customize_Section( $this, $id, $args );
3067
		}
3068
3069
		$this->sections[ $section->id ] = $section;
3070
		return $section;
3071
	}
3072
3073
	/**
3074
	 * Retrieve a customize section.
3075
	 *
3076
	 * @since 3.4.0
3077
	 *
3078
	 * @param string $id Section ID.
3079
	 * @return WP_Customize_Section|void The section, if set.
3080
	 */
3081
	public function get_section( $id ) {
3082
		if ( isset( $this->sections[ $id ] ) )
3083
			return $this->sections[ $id ];
3084
	}
3085
3086
	/**
3087
	 * Remove a customize section.
3088
	 *
3089
	 * @since 3.4.0
3090
	 *
3091
	 * @param string $id Section ID.
3092
	 */
3093
	public function remove_section( $id ) {
3094
		unset( $this->sections[ $id ] );
3095
	}
3096
3097
	/**
3098
	 * Register a customize section type.
3099
	 *
3100
	 * Registered types are eligible to be rendered via JS and created dynamically.
3101
	 *
3102
	 * @since 4.3.0
3103
	 * @access public
3104
	 *
3105
	 * @see WP_Customize_Section
3106
	 *
3107
	 * @param string $section Name of a custom section which is a subclass of WP_Customize_Section.
3108
	 */
3109
	public function register_section_type( $section ) {
3110
		$this->registered_section_types[] = $section;
3111
	}
3112
3113
	/**
3114
	 * Render JS templates for all registered section types.
3115
	 *
3116
	 * @since 4.3.0
3117
	 * @access public
3118
	 */
3119
	public function render_section_templates() {
3120
		foreach ( $this->registered_section_types as $section_type ) {
3121
			$section = new $section_type( $this, 'temp', array() );
3122
			$section->print_template();
3123
		}
3124
	}
3125
3126
	/**
3127
	 * Add a customize control.
3128
	 *
3129
	 * @since 3.4.0
3130
	 * @since 4.5.0 Return added WP_Customize_Control instance.
3131
	 * @access public
3132
	 *
3133
	 * @param WP_Customize_Control|string $id   Customize Control object, or ID.
3134
	 * @param array                       $args {
3135
	 *  Optional. Array of properties for the new Control object. Default empty array.
3136
	 *
3137
	 *  @type array        $settings              All settings tied to the control. If undefined, defaults to `$setting`.
3138
	 *                                            IDs in the array correspond to the ID of a registered `WP_Customize_Setting`.
3139
	 *  @type string       $setting               The primary setting for the control (if there is one). Default is 'default'.
3140
	 *  @type string       $capability            Capability required to use this control. Normally derived from `$settings`.
3141
	 *  @type int          $priority              Order priority to load the control. Default 10.
3142
	 *  @type string       $section               The section this control belongs to. Default empty.
3143
	 *  @type string       $label                 Label for the control. Default empty.
3144
	 *  @type string       $description           Description for the control. Default empty.
3145
	 *  @type array        $choices               List of choices for 'radio' or 'select' type controls, where values
3146
	 *                                            are the keys, and labels are the values. Default empty array.
3147
	 *  @type array        $input_attrs           List of custom input attributes for control output, where attribute
3148
	 *                                            names are the keys and values are the values. Default empty array.
3149
	 *  @type bool         $allow_addition        Show UI for adding new content, currently only used for the
3150
	 *                                            dropdown-pages control. Default false.
3151
	 *  @type string       $type                  The type of the control. Default 'text'.
3152
	 *  @type callback     $active_callback       Active callback.
3153
	 * }
3154
	 * @return WP_Customize_Control             The instance of the control that was added.
3155
	 */
3156
	public function add_control( $id, $args = array() ) {
3157
		if ( $id instanceof WP_Customize_Control ) {
3158
			$control = $id;
3159
		} else {
3160
			$control = new WP_Customize_Control( $this, $id, $args );
3161
		}
3162
3163
		$this->controls[ $control->id ] = $control;
3164
		return $control;
3165
	}
3166
3167
	/**
3168
	 * Retrieve a customize control.
3169
	 *
3170
	 * @since 3.4.0
3171
	 *
3172
	 * @param string $id ID of the control.
3173
	 * @return WP_Customize_Control|void The control object, if set.
3174
	 */
3175
	public function get_control( $id ) {
3176
		if ( isset( $this->controls[ $id ] ) )
3177
			return $this->controls[ $id ];
3178
	}
3179
3180
	/**
3181
	 * Remove a customize control.
3182
	 *
3183
	 * @since 3.4.0
3184
	 *
3185
	 * @param string $id ID of the control.
3186
	 */
3187
	public function remove_control( $id ) {
3188
		unset( $this->controls[ $id ] );
3189
	}
3190
3191
	/**
3192
	 * Register a customize control type.
3193
	 *
3194
	 * Registered types are eligible to be rendered via JS and created dynamically.
3195
	 *
3196
	 * @since 4.1.0
3197
	 * @access public
3198
	 *
3199
	 * @param string $control Name of a custom control which is a subclass of
3200
	 *                        WP_Customize_Control.
3201
	 */
3202
	public function register_control_type( $control ) {
3203
		$this->registered_control_types[] = $control;
3204
	}
3205
3206
	/**
3207
	 * Render JS templates for all registered control types.
3208
	 *
3209
	 * @since 4.1.0
3210
	 * @access public
3211
	 */
3212
	public function render_control_templates() {
3213
		foreach ( $this->registered_control_types as $control_type ) {
3214
			$control = new $control_type( $this, 'temp', array(
3215
				'settings' => array(),
3216
			) );
3217
			$control->print_template();
3218
		}
3219
		?>
3220
		<script type="text/html" id="tmpl-customize-control-notifications">
3221
			<ul>
3222
				<# _.each( data.notifications, function( notification ) { #>
3223
					<li class="notice notice-{{ notification.type || 'info' }} {{ data.altNotice ? 'notice-alt' : '' }}" data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{{ notification.message || notification.code }}}</li>
3224
				<# } ); #>
3225
			</ul>
3226
		</script>
3227
		<?php
3228
	}
3229
3230
	/**
3231
	 * Helper function to compare two objects by priority, ensuring sort stability via instance_number.
3232
	 *
3233
	 * @since 3.4.0
3234
	 * @deprecated 4.7.0 Use wp_list_sort()
3235
	 *
3236
	 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $a Object A.
3237
	 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $b Object B.
3238
	 * @return int
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|double?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
3239
	 */
3240
	protected function _cmp_priority( $a, $b ) {
3241
		_deprecated_function( __METHOD__, '4.7.0', 'wp_list_sort' );
3242
3243
		if ( $a->priority === $b->priority ) {
3244
			return $a->instance_number - $b->instance_number;
3245
		} else {
3246
			return $a->priority - $b->priority;
3247
		}
3248
	}
3249
3250
	/**
3251
	 * Prepare panels, sections, and controls.
3252
	 *
3253
	 * For each, check if required related components exist,
3254
	 * whether the user has the necessary capabilities,
3255
	 * and sort by priority.
3256
	 *
3257
	 * @since 3.4.0
3258
	 */
3259
	public function prepare_controls() {
3260
3261
		$controls = array();
3262
		$this->controls = wp_list_sort( $this->controls, array(
3263
			'priority'        => 'ASC',
3264
			'instance_number' => 'ASC',
3265
		), 'ASC', true );
3266
3267
		foreach ( $this->controls as $id => $control ) {
3268
			if ( ! isset( $this->sections[ $control->section ] ) || ! $control->check_capabilities() ) {
3269
				continue;
3270
			}
3271
3272
			$this->sections[ $control->section ]->controls[] = $control;
3273
			$controls[ $id ] = $control;
3274
		}
3275
		$this->controls = $controls;
3276
3277
		// Prepare sections.
3278
		$this->sections = wp_list_sort( $this->sections, array(
3279
			'priority'        => 'ASC',
3280
			'instance_number' => 'ASC',
3281
		), 'ASC', true );
3282
		$sections = array();
3283
3284
		foreach ( $this->sections as $section ) {
3285
			if ( ! $section->check_capabilities() ) {
3286
				continue;
3287
			}
3288
3289
3290
			$section->controls = wp_list_sort( $section->controls, array(
3291
				'priority'        => 'ASC',
3292
				'instance_number' => 'ASC',
3293
			) );
3294
3295
			if ( ! $section->panel ) {
3296
				// Top-level section.
3297
				$sections[ $section->id ] = $section;
3298
			} else {
3299
				// This section belongs to a panel.
3300
				if ( isset( $this->panels [ $section->panel ] ) ) {
3301
					$this->panels[ $section->panel ]->sections[ $section->id ] = $section;
3302
				}
3303
			}
3304
		}
3305
		$this->sections = $sections;
3306
3307
		// Prepare panels.
3308
		$this->panels = wp_list_sort( $this->panels, array(
3309
			'priority'        => 'ASC',
3310
			'instance_number' => 'ASC',
3311
		), 'ASC', true );
3312
		$panels = array();
3313
3314
		foreach ( $this->panels as $panel ) {
3315
			if ( ! $panel->check_capabilities() ) {
3316
				continue;
3317
			}
3318
3319
			$panel->sections = wp_list_sort( $panel->sections, array(
3320
				'priority'        => 'ASC',
3321
				'instance_number' => 'ASC',
3322
			), 'ASC', true );
3323
			$panels[ $panel->id ] = $panel;
3324
		}
3325
		$this->panels = $panels;
3326
3327
		// Sort panels and top-level sections together.
3328
		$this->containers = array_merge( $this->panels, $this->sections );
3329
		$this->containers = wp_list_sort( $this->containers, array(
3330
			'priority'        => 'ASC',
3331
			'instance_number' => 'ASC',
3332
		), 'ASC', true );
3333
	}
3334
3335
	/**
3336
	 * Enqueue scripts for customize controls.
3337
	 *
3338
	 * @since 3.4.0
3339
	 */
3340
	public function enqueue_control_scripts() {
3341
		foreach ( $this->controls as $control ) {
3342
			$control->enqueue();
3343
		}
3344
	}
3345
3346
	/**
3347
	 * Determine whether the user agent is iOS.
3348
	 *
3349
	 * @since 4.4.0
3350
	 * @access public
3351
	 *
3352
	 * @return bool Whether the user agent is iOS.
3353
	 */
3354
	public function is_ios() {
3355
		return wp_is_mobile() && preg_match( '/iPad|iPod|iPhone/', $_SERVER['HTTP_USER_AGENT'] );
3356
	}
3357
3358
	/**
3359
	 * Get the template string for the Customizer pane document title.
3360
	 *
3361
	 * @since 4.4.0
3362
	 * @access public
3363
	 *
3364
	 * @return string The template string for the document title.
3365
	 */
3366
	public function get_document_title_template() {
3367
		if ( $this->is_theme_active() ) {
3368
			/* translators: %s: document title from the preview */
3369
			$document_title_tmpl = __( 'Customize: %s' );
3370
		} else {
3371
			/* translators: %s: document title from the preview */
3372
			$document_title_tmpl = __( 'Live Preview: %s' );
3373
		}
3374
		$document_title_tmpl = html_entity_decode( $document_title_tmpl, ENT_QUOTES, 'UTF-8' ); // Because exported to JS and assigned to document.title.
3375
		return $document_title_tmpl;
3376
	}
3377
3378
	/**
3379
	 * Set the initial URL to be previewed.
3380
	 *
3381
	 * URL is validated.
3382
	 *
3383
	 * @since 4.4.0
3384
	 * @access public
3385
	 *
3386
	 * @param string $preview_url URL to be previewed.
3387
	 */
3388
	public function set_preview_url( $preview_url ) {
3389
		$preview_url = esc_url_raw( $preview_url );
3390
		$this->preview_url = wp_validate_redirect( $preview_url, home_url( '/' ) );
3391
	}
3392
3393
	/**
3394
	 * Get the initial URL to be previewed.
3395
	 *
3396
	 * @since 4.4.0
3397
	 * @access public
3398
	 *
3399
	 * @return string URL being previewed.
3400
	 */
3401
	public function get_preview_url() {
3402
		if ( empty( $this->preview_url ) ) {
3403
			$preview_url = home_url( '/' );
3404
		} else {
3405
			$preview_url = $this->preview_url;
3406
		}
3407
		return $preview_url;
3408
	}
3409
3410
	/**
3411
	 * Determines whether the admin and the frontend are on different domains.
3412
	 *
3413
	 * @since 4.7.0
3414
	 * @access public
3415
	 *
3416
	 * @return bool Whether cross-domain.
3417
	 */
3418
	public function is_cross_domain() {
3419
		$admin_origin = wp_parse_url( admin_url() );
3420
		$home_origin = wp_parse_url( home_url() );
3421
		$cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
3422
		return $cross_domain;
3423
	}
3424
3425
	/**
3426
	 * Get URLs allowed to be previewed.
3427
	 *
3428
	 * If the front end and the admin are served from the same domain, load the
3429
	 * preview over ssl if the Customizer is being loaded over ssl. This avoids
3430
	 * insecure content warnings. This is not attempted if the admin and front end
3431
	 * are on different domains to avoid the case where the front end doesn't have
3432
	 * ssl certs. Domain mapping plugins can allow other urls in these conditions
3433
	 * using the customize_allowed_urls filter.
3434
	 *
3435
	 * @since 4.7.0
3436
	 * @access public
3437
	 *
3438
	 * @returns array Allowed URLs.
3439
	 */
3440
	public function get_allowed_urls() {
3441
		$allowed_urls = array( home_url( '/' ) );
3442
3443
		if ( is_ssl() && ! $this->is_cross_domain() ) {
3444
			$allowed_urls[] = home_url( '/', 'https' );
3445
		}
3446
3447
		/**
3448
		 * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.
3449
		 *
3450
		 * @since 3.4.0
3451
		 *
3452
		 * @param array $allowed_urls An array of allowed URLs.
3453
		 */
3454
		$allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );
3455
3456
		return $allowed_urls;
3457
	}
3458
3459
	/**
3460
	 * Get messenger channel.
3461
	 *
3462
	 * @since 4.7.0
3463
	 * @access public
3464
	 *
3465
	 * @return string Messenger channel.
3466
	 */
3467
	public function get_messenger_channel() {
3468
		return $this->messenger_channel;
3469
	}
3470
3471
	/**
3472
	 * Set URL to link the user to when closing the Customizer.
3473
	 *
3474
	 * URL is validated.
3475
	 *
3476
	 * @since 4.4.0
3477
	 * @access public
3478
	 *
3479
	 * @param string $return_url URL for return link.
3480
	 */
3481
	public function set_return_url( $return_url ) {
3482
		$return_url = esc_url_raw( $return_url );
3483
		$return_url = remove_query_arg( wp_removable_query_args(), $return_url );
3484
		$return_url = wp_validate_redirect( $return_url );
0 ignored issues
show
Bug introduced by
It seems like $return_url can also be of type boolean; however, wp_validate_redirect() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
3485
		$this->return_url = $return_url;
3486
	}
3487
3488
	/**
3489
	 * Get URL to link the user to when closing the Customizer.
3490
	 *
3491
	 * @since 4.4.0
3492
	 * @access public
3493
	 *
3494
	 * @return string URL for link to close Customizer.
3495
	 */
3496
	public function get_return_url() {
3497
		$referer = wp_get_referer();
3498
		$excluded_referer_basenames = array( 'customize.php', 'wp-login.php' );
3499
3500
		if ( $this->return_url ) {
3501
			$return_url = $this->return_url;
3502
		} else if ( $referer && ! in_array( basename( parse_url( $referer, PHP_URL_PATH ) ), $excluded_referer_basenames, true ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $referer of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
3503
			$return_url = $referer;
3504
		} else if ( $this->preview_url ) {
3505
			$return_url = $this->preview_url;
3506
		} else {
3507
			$return_url = home_url( '/' );
3508
		}
3509
		return $return_url;
3510
	}
3511
3512
	/**
3513
	 * Set the autofocused constructs.
3514
	 *
3515
	 * @since 4.4.0
3516
	 * @access public
3517
	 *
3518
	 * @param array $autofocus {
3519
	 *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
3520
	 *
3521
	 *     @type string [$control]  ID for control to be autofocused.
3522
	 *     @type string [$section]  ID for section to be autofocused.
3523
	 *     @type string [$panel]    ID for panel to be autofocused.
3524
	 * }
3525
	 */
3526
	public function set_autofocus( $autofocus ) {
3527
		$this->autofocus = array_filter( wp_array_slice_assoc( $autofocus, array( 'panel', 'section', 'control' ) ), 'is_string' );
3528
	}
3529
3530
	/**
3531
	 * Get the autofocused constructs.
3532
	 *
3533
	 * @since 4.4.0
3534
	 * @access public
3535
	 *
3536
	 * @return array {
3537
	 *     Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
3538
	 *
3539
	 *     @type string [$control]  ID for control to be autofocused.
3540
	 *     @type string [$section]  ID for section to be autofocused.
3541
	 *     @type string [$panel]    ID for panel to be autofocused.
3542
	 * }
3543
	 */
3544
	public function get_autofocus() {
3545
		return $this->autofocus;
3546
	}
3547
3548
	/**
3549
	 * Get nonces for the Customizer.
3550
	 *
3551
	 * @since 4.5.0
3552
	 *
3553
	 * @return array Nonces.
3554
	 */
3555
	public function get_nonces() {
3556
		$nonces = array(
3557
			'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
3558
			'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
3559
		);
3560
3561
		/**
3562
		 * Filters nonces for Customizer.
3563
		 *
3564
		 * @since 4.2.0
3565
		 *
3566
		 * @param array                $nonces Array of refreshed nonces for save and
3567
		 *                                     preview actions.
3568
		 * @param WP_Customize_Manager $this   WP_Customize_Manager instance.
3569
		 */
3570
		$nonces = apply_filters( 'customize_refresh_nonces', $nonces, $this );
3571
3572
		return $nonces;
3573
	}
3574
3575
	/**
3576
	 * Print JavaScript settings for parent window.
3577
	 *
3578
	 * @since 4.4.0
3579
	 */
3580
	public function customize_pane_settings() {
3581
3582
		$login_url = add_query_arg( array(
3583
			'interim-login' => 1,
3584
			'customize-login' => 1,
3585
		), wp_login_url() );
3586
3587
		// Ensure dirty flags are set for modified settings.
3588
		foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) {
3589
			$setting = $this->get_setting( $setting_id );
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $setting is correct as $this->get_setting($setting_id) (which targets WP_Customize_Manager::get_setting()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
3590
			if ( $setting ) {
3591
				$setting->dirty = true;
3592
			}
3593
		}
3594
3595
		// Prepare Customizer settings to pass to JavaScript.
3596
		$settings = array(
3597
			'changeset' => array(
3598
				'uuid' => $this->changeset_uuid(),
3599
				'status' => $this->changeset_post_id() ? get_post_status( $this->changeset_post_id() ) : '',
3600
			),
3601
			'timeouts' => array(
3602
				'windowRefresh' => 250,
3603
				'changesetAutoSave' => AUTOSAVE_INTERVAL * 1000,
3604
				'keepAliveCheck' => 2500,
3605
				'reflowPaneContents' => 100,
3606
				'previewFrameSensitivity' => 2000,
3607
			),
3608
			'theme'    => array(
3609
				'stylesheet' => $this->get_stylesheet(),
3610
				'active'     => $this->is_theme_active(),
3611
			),
3612
			'url'      => array(
3613
				'preview'       => esc_url_raw( $this->get_preview_url() ),
3614
				'parent'        => esc_url_raw( admin_url() ),
3615
				'activated'     => esc_url_raw( home_url( '/' ) ),
3616
				'ajax'          => esc_url_raw( admin_url( 'admin-ajax.php', 'relative' ) ),
3617
				'allowed'       => array_map( 'esc_url_raw', $this->get_allowed_urls() ),
3618
				'isCrossDomain' => $this->is_cross_domain(),
3619
				'home'          => esc_url_raw( home_url( '/' ) ),
3620
				'login'         => esc_url_raw( $login_url ),
3621
			),
3622
			'browser'  => array(
3623
				'mobile' => wp_is_mobile(),
3624
				'ios'    => $this->is_ios(),
3625
			),
3626
			'panels'   => array(),
3627
			'sections' => array(),
3628
			'nonce'    => $this->get_nonces(),
3629
			'autofocus' => $this->get_autofocus(),
3630
			'documentTitleTmpl' => $this->get_document_title_template(),
3631
			'previewableDevices' => $this->get_previewable_devices(),
3632
		);
3633
3634
		// Prepare Customize Section objects to pass to JavaScript.
3635
		foreach ( $this->sections() as $id => $section ) {
3636
			if ( $section->check_capabilities() ) {
3637
				$settings['sections'][ $id ] = $section->json();
3638
			}
3639
		}
3640
3641
		// Prepare Customize Panel objects to pass to JavaScript.
3642 View Code Duplication
		foreach ( $this->panels() as $panel_id => $panel ) {
3643
			if ( $panel->check_capabilities() ) {
3644
				$settings['panels'][ $panel_id ] = $panel->json();
3645
				foreach ( $panel->sections as $section_id => $section ) {
3646
					if ( $section->check_capabilities() ) {
3647
						$settings['sections'][ $section_id ] = $section->json();
3648
					}
3649
				}
3650
			}
3651
		}
3652
3653
		?>
3654
		<script type="text/javascript">
3655
			var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
3656
			_wpCustomizeSettings.controls = {};
3657
			_wpCustomizeSettings.settings = {};
3658
			<?php
3659
3660
			// Serialize settings one by one to improve memory usage.
3661
			echo "(function ( s ){\n";
3662 View Code Duplication
			foreach ( $this->settings() as $setting ) {
3663
				if ( $setting->check_capabilities() ) {
3664
					printf(
3665
						"s[%s] = %s;\n",
3666
						wp_json_encode( $setting->id ),
3667
						wp_json_encode( $setting->json() )
3668
					);
3669
				}
3670
			}
3671
			echo "})( _wpCustomizeSettings.settings );\n";
3672
3673
			// Serialize controls one by one to improve memory usage.
3674
			echo "(function ( c ){\n";
3675
			foreach ( $this->controls() as $control ) {
3676
				if ( $control->check_capabilities() ) {
3677
					printf(
3678
						"c[%s] = %s;\n",
3679
						wp_json_encode( $control->id ),
3680
						wp_json_encode( $control->json() )
3681
					);
3682
				}
3683
			}
3684
			echo "})( _wpCustomizeSettings.controls );\n";
3685
		?>
3686
		</script>
3687
		<?php
3688
	}
3689
3690
	/**
3691
	 * Returns a list of devices to allow previewing.
3692
	 *
3693
	 * @since 4.5.0
3694
	 *
3695
	 * @return array List of devices with labels and default setting.
3696
	 */
3697
	public function get_previewable_devices() {
3698
		$devices = array(
3699
			'desktop' => array(
3700
				'label' => __( 'Enter desktop preview mode' ),
3701
				'default' => true,
3702
			),
3703
			'tablet' => array(
3704
				'label' => __( 'Enter tablet preview mode' ),
3705
			),
3706
			'mobile' => array(
3707
				'label' => __( 'Enter mobile preview mode' ),
3708
			),
3709
		);
3710
3711
		/**
3712
		 * Filters the available devices to allow previewing in the Customizer.
3713
		 *
3714
		 * @since 4.5.0
3715
		 *
3716
		 * @see WP_Customize_Manager::get_previewable_devices()
3717
		 *
3718
		 * @param array $devices List of devices with labels and default setting.
3719
		 */
3720
		$devices = apply_filters( 'customize_previewable_devices', $devices );
3721
3722
		return $devices;
3723
	}
3724
3725
	/**
3726
	 * Register some default controls.
3727
	 *
3728
	 * @since 3.4.0
3729
	 */
3730
	public function register_controls() {
3731
3732
		/* Panel, Section, and Control Types */
3733
		$this->register_panel_type( 'WP_Customize_Panel' );
3734
		$this->register_section_type( 'WP_Customize_Section' );
3735
		$this->register_section_type( 'WP_Customize_Sidebar_Section' );
3736
		$this->register_control_type( 'WP_Customize_Color_Control' );
3737
		$this->register_control_type( 'WP_Customize_Media_Control' );
3738
		$this->register_control_type( 'WP_Customize_Upload_Control' );
3739
		$this->register_control_type( 'WP_Customize_Image_Control' );
3740
		$this->register_control_type( 'WP_Customize_Background_Image_Control' );
3741
		$this->register_control_type( 'WP_Customize_Background_Position_Control' );
3742
		$this->register_control_type( 'WP_Customize_Cropped_Image_Control' );
3743
		$this->register_control_type( 'WP_Customize_Site_Icon_Control' );
3744
		$this->register_control_type( 'WP_Customize_Theme_Control' );
3745
3746
		/* Themes */
3747
3748
		$this->add_section( new WP_Customize_Themes_Section( $this, 'themes', array(
3749
			'title'      => $this->theme()->display( 'Name' ),
3750
			'capability' => 'switch_themes',
3751
			'priority'   => 0,
3752
		) ) );
3753
3754
		// Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
3755
		$this->add_setting( new WP_Customize_Filter_Setting( $this, 'active_theme', array(
3756
			'capability' => 'switch_themes',
3757
		) ) );
3758
3759
		require_once( ABSPATH . 'wp-admin/includes/theme.php' );
3760
3761
		// Theme Controls.
3762
3763
		// Add a control for the active/original theme.
3764
		if ( ! $this->is_theme_active() ) {
3765
			$themes = wp_prepare_themes_for_js( array( wp_get_theme( $this->original_stylesheet ) ) );
3766
			$active_theme = current( $themes );
3767
			$active_theme['isActiveTheme'] = true;
3768
			$this->add_control( new WP_Customize_Theme_Control( $this, $active_theme['id'], array(
3769
				'theme'    => $active_theme,
3770
				'section'  => 'themes',
3771
				'settings' => 'active_theme',
3772
			) ) );
3773
		}
3774
3775
		$themes = wp_prepare_themes_for_js();
3776
		foreach ( $themes as $theme ) {
3777
			if ( $theme['active'] || $theme['id'] === $this->original_stylesheet ) {
3778
				continue;
3779
			}
3780
3781
			$theme_id = 'theme_' . $theme['id'];
3782
			$theme['isActiveTheme'] = false;
3783
			$this->add_control( new WP_Customize_Theme_Control( $this, $theme_id, array(
3784
				'theme'    => $theme,
3785
				'section'  => 'themes',
3786
				'settings' => 'active_theme',
3787
			) ) );
3788
		}
3789
3790
		/* Site Identity */
3791
3792
		$this->add_section( 'title_tagline', array(
3793
			'title'    => __( 'Site Identity' ),
3794
			'priority' => 20,
3795
		) );
3796
3797
		$this->add_setting( 'blogname', array(
3798
			'default'    => get_option( 'blogname' ),
3799
			'type'       => 'option',
3800
			'capability' => 'manage_options',
3801
		) );
3802
3803
		$this->add_control( 'blogname', array(
3804
			'label'      => __( 'Site Title' ),
3805
			'section'    => 'title_tagline',
3806
		) );
3807
3808
		$this->add_setting( 'blogdescription', array(
3809
			'default'    => get_option( 'blogdescription' ),
3810
			'type'       => 'option',
3811
			'capability' => 'manage_options',
3812
		) );
3813
3814
		$this->add_control( 'blogdescription', array(
3815
			'label'      => __( 'Tagline' ),
3816
			'section'    => 'title_tagline',
3817
		) );
3818
3819
		// Add a setting to hide header text if the theme doesn't support custom headers.
3820
		if ( ! current_theme_supports( 'custom-header', 'header-text' ) ) {
3821
			$this->add_setting( 'header_text', array(
3822
				'theme_supports'    => array( 'custom-logo', 'header-text' ),
3823
				'default'           => 1,
3824
				'sanitize_callback' => 'absint',
3825
			) );
3826
3827
			$this->add_control( 'header_text', array(
3828
				'label'    => __( 'Display Site Title and Tagline' ),
3829
				'section'  => 'title_tagline',
3830
				'settings' => 'header_text',
3831
				'type'     => 'checkbox',
3832
			) );
3833
		}
3834
3835
		$this->add_setting( 'site_icon', array(
3836
			'type'       => 'option',
3837
			'capability' => 'manage_options',
3838
			'transport'  => 'postMessage', // Previewed with JS in the Customizer controls window.
3839
		) );
3840
3841
		$this->add_control( new WP_Customize_Site_Icon_Control( $this, 'site_icon', array(
3842
			'label'       => __( 'Site Icon' ),
3843
			'description' => sprintf(
3844
				/* translators: %s: site icon size in pixels */
3845
				__( 'The Site Icon is used as a browser and app icon for your site. Icons must be square, and at least %s pixels wide and tall.' ),
3846
				'<strong>512</strong>'
3847
			),
3848
			'section'     => 'title_tagline',
3849
			'priority'    => 60,
3850
			'height'      => 512,
3851
			'width'       => 512,
3852
		) ) );
3853
3854
		$this->add_setting( 'custom_logo', array(
3855
			'theme_supports' => array( 'custom-logo' ),
3856
			'transport'      => 'postMessage',
3857
		) );
3858
3859
		$custom_logo_args = get_theme_support( 'custom-logo' );
3860
		$this->add_control( new WP_Customize_Cropped_Image_Control( $this, 'custom_logo', array(
3861
			'label'         => __( 'Logo' ),
3862
			'section'       => 'title_tagline',
3863
			'priority'      => 8,
3864
			'height'        => $custom_logo_args[0]['height'],
3865
			'width'         => $custom_logo_args[0]['width'],
3866
			'flex_height'   => $custom_logo_args[0]['flex-height'],
3867
			'flex_width'    => $custom_logo_args[0]['flex-width'],
3868
			'button_labels' => array(
3869
				'select'       => __( 'Select logo' ),
3870
				'change'       => __( 'Change logo' ),
3871
				'remove'       => __( 'Remove' ),
3872
				'default'      => __( 'Default' ),
3873
				'placeholder'  => __( 'No logo selected' ),
3874
				'frame_title'  => __( 'Select logo' ),
3875
				'frame_button' => __( 'Choose logo' ),
3876
			),
3877
		) ) );
3878
3879
		$this->selective_refresh->add_partial( 'custom_logo', array(
3880
			'settings'            => array( 'custom_logo' ),
3881
			'selector'            => '.custom-logo-link',
3882
			'render_callback'     => array( $this, '_render_custom_logo_partial' ),
3883
			'container_inclusive' => true,
3884
		) );
3885
3886
		/* Colors */
3887
3888
		$this->add_section( 'colors', array(
3889
			'title'          => __( 'Colors' ),
3890
			'priority'       => 40,
3891
		) );
3892
3893
		$this->add_setting( 'header_textcolor', array(
3894
			'theme_supports' => array( 'custom-header', 'header-text' ),
3895
			'default'        => get_theme_support( 'custom-header', 'default-text-color' ),
3896
3897
			'sanitize_callback'    => array( $this, '_sanitize_header_textcolor' ),
3898
			'sanitize_js_callback' => 'maybe_hash_hex_color',
3899
		) );
3900
3901
		// Input type: checkbox
3902
		// With custom value
3903
		$this->add_control( 'display_header_text', array(
3904
			'settings' => 'header_textcolor',
3905
			'label'    => __( 'Display Site Title and Tagline' ),
3906
			'section'  => 'title_tagline',
3907
			'type'     => 'checkbox',
3908
			'priority' => 40,
3909
		) );
3910
3911
		$this->add_control( new WP_Customize_Color_Control( $this, 'header_textcolor', array(
3912
			'label'   => __( 'Header Text Color' ),
3913
			'section' => 'colors',
3914
		) ) );
3915
3916
		// Input type: Color
3917
		// With sanitize_callback
3918
		$this->add_setting( 'background_color', array(
3919
			'default'        => get_theme_support( 'custom-background', 'default-color' ),
3920
			'theme_supports' => 'custom-background',
3921
3922
			'sanitize_callback'    => 'sanitize_hex_color_no_hash',
3923
			'sanitize_js_callback' => 'maybe_hash_hex_color',
3924
		) );
3925
3926
		$this->add_control( new WP_Customize_Color_Control( $this, 'background_color', array(
3927
			'label'   => __( 'Background Color' ),
3928
			'section' => 'colors',
3929
		) ) );
3930
3931
		/* Custom Header */
3932
3933
		if ( current_theme_supports( 'custom-header', 'video' ) ) {
3934
			$title = __( 'Header Media' );
3935
			$description = '<p>' . __( 'If you add a video, the image will be used as a fallback while the video loads.' ) . '</p>';
3936
3937
			// @todo Customizer sections should support having notifications just like controls do. See <https://core.trac.wordpress.org/ticket/38794>.
3938
			$description .= '<div class="customize-control-notifications-container header-video-not-currently-previewable" style="display: none"><ul>';
3939
			$description .= '<li class="notice notice-info">' . __( 'This theme doesn\'t support video headers on this page. Navigate to the front page or another page that supports video headers.' ) . '</li>';
3940
			$description .= '</ul></div>';
3941
			$width = absint( get_theme_support( 'custom-header', 'width' ) );
3942
			$height = absint( get_theme_support( 'custom-header', 'height' ) );
3943
			if ( $width && $height ) {
3944
				$control_description = sprintf(
3945
					/* translators: 1: .mp4, 2: header size in pixels */
3946
					__( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends dimensions of %2$s pixels.' ),
3947
					'<code>.mp4</code>',
3948
					sprintf( '<strong>%s &times; %s</strong>', $width, $height )
3949
				);
3950
			} elseif ( $width ) {
3951
				$control_description = sprintf(
3952
					/* translators: 1: .mp4, 2: header width in pixels */
3953
					__( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a width of %2$s pixels.' ),
3954
					'<code>.mp4</code>',
3955
					sprintf( '<strong>%s</strong>', $width )
3956
				);
3957
			} else {
3958
				$control_description = sprintf(
3959
					/* translators: 1: .mp4, 2: header height in pixels */
3960
					__( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a height of %2$s pixels.' ),
3961
					'<code>.mp4</code>',
3962
					sprintf( '<strong>%s</strong>', $height )
3963
				);
3964
			}
3965
		} else {
3966
			$title = __( 'Header Image' );
3967
			$description = '';
3968
			$control_description = '';
3969
		}
3970
3971
		$this->add_section( 'header_image', array(
3972
			'title'          => $title,
3973
			'description'    => $description,
3974
			'theme_supports' => 'custom-header',
3975
			'priority'       => 60,
3976
		) );
3977
3978
		$this->add_setting( 'header_video', array(
3979
			'theme_supports'    => array( 'custom-header', 'video' ),
3980
			'transport'         => 'postMessage',
3981
			'sanitize_callback' => 'absint',
3982
			'validate_callback' => array( $this, '_validate_header_video' ),
3983
		) );
3984
3985
		$this->add_setting( 'external_header_video', array(
3986
			'theme_supports'    => array( 'custom-header', 'video' ),
3987
			'transport'         => 'postMessage',
3988
			'sanitize_callback' => array( $this, '_sanitize_external_header_video' ),
3989
			'validate_callback' => array( $this, '_validate_external_header_video' ),
3990
		) );
3991
3992
		$this->add_setting( new WP_Customize_Filter_Setting( $this, 'header_image', array(
3993
			'default'        => sprintf( get_theme_support( 'custom-header', 'default-image' ), get_template_directory_uri(), get_stylesheet_directory_uri() ),
3994
			'theme_supports' => 'custom-header',
3995
		) ) );
3996
3997
		$this->add_setting( new WP_Customize_Header_Image_Setting( $this, 'header_image_data', array(
3998
			'theme_supports' => 'custom-header',
3999
		) ) );
4000
4001
		/*
4002
		 * Switch image settings to postMessage when video support is enabled since
4003
		 * it entails that the_custom_header_markup() will be used, and thus selective
4004
		 * refresh can be utilized.
4005
		 */
4006
		if ( current_theme_supports( 'custom-header', 'video' ) ) {
4007
			$this->get_setting( 'header_image' )->transport = 'postMessage';
4008
			$this->get_setting( 'header_image_data' )->transport = 'postMessage';
4009
		}
4010
4011
		$this->add_control( new WP_Customize_Media_Control( $this, 'header_video', array(
4012
			'theme_supports' => array( 'custom-header', 'video' ),
4013
			'label'          => __( 'Header Video' ),
4014
			'description'    => $control_description,
4015
			'section'        => 'header_image',
4016
			'mime_type'      => 'video',
4017
			// @todo These button_labels can be removed once WP_Customize_Media_Control provides mime_type-specific labels automatically. See <https://core.trac.wordpress.org/ticket/38796>.
4018
			'button_labels'  => array(
4019
				'select'       => __( 'Select Video' ),
4020
				'change'       => __( 'Change Video' ),
4021
				'placeholder'  => __( 'No video selected' ),
4022
				'frame_title'  => __( 'Select Video' ),
4023
				'frame_button' => __( 'Choose Video' ),
4024
			),
4025
			'active_callback' => 'is_header_video_active',
4026
		) ) );
4027
4028
		$this->add_control( 'external_header_video', array(
4029
			'theme_supports' => array( 'custom-header', 'video' ),
4030
			'type'           => 'url',
4031
			'description'    => __( 'Or, enter a YouTube URL:' ),
4032
			'section'        => 'header_image',
4033
			'active_callback' => 'is_header_video_active',
4034
		) );
4035
4036
		$this->add_control( new WP_Customize_Header_Image_Control( $this ) );
4037
4038
		$this->selective_refresh->add_partial( 'custom_header', array(
4039
			'selector'            => '#wp-custom-header',
4040
			'render_callback'     => 'the_custom_header_markup',
4041
			'settings'            => array( 'header_video', 'external_header_video', 'header_image' ), // The image is used as a video fallback here.
4042
			'container_inclusive' => true,
4043
		) );
4044
4045
		/* Custom Background */
4046
4047
		$this->add_section( 'background_image', array(
4048
			'title'          => __( 'Background Image' ),
4049
			'theme_supports' => 'custom-background',
4050
			'priority'       => 80,
4051
		) );
4052
4053
		$this->add_setting( 'background_image', array(
4054
			'default'        => get_theme_support( 'custom-background', 'default-image' ),
4055
			'theme_supports' => 'custom-background',
4056
			'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4057
		) );
4058
4059
		$this->add_setting( new WP_Customize_Background_Image_Setting( $this, 'background_image_thumb', array(
4060
			'theme_supports' => 'custom-background',
4061
			'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4062
		) ) );
4063
4064
		$this->add_control( new WP_Customize_Background_Image_Control( $this ) );
4065
4066
		$this->add_setting( 'background_preset', array(
4067
			'default'        => get_theme_support( 'custom-background', 'default-preset' ),
4068
			'theme_supports' => 'custom-background',
4069
			'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4070
		) );
4071
4072
		$this->add_control( 'background_preset', array(
4073
			'label'      => _x( 'Preset', 'Background Preset' ),
4074
			'section'    => 'background_image',
4075
			'type'       => 'select',
4076
			'choices'    => array(
4077
				'default' => _x( 'Default', 'Default Preset' ),
4078
				'fill'    => __( 'Fill Screen' ),
4079
				'fit'     => __( 'Fit to Screen' ),
4080
				'repeat'  => _x( 'Repeat', 'Repeat Image' ),
4081
				'custom'  => _x( 'Custom', 'Custom Preset' ),
4082
			),
4083
		) );
4084
4085
		$this->add_setting( 'background_position_x', array(
4086
			'default'        => get_theme_support( 'custom-background', 'default-position-x' ),
4087
			'theme_supports' => 'custom-background',
4088
			'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4089
		) );
4090
4091
		$this->add_setting( 'background_position_y', array(
4092
			'default'        => get_theme_support( 'custom-background', 'default-position-y' ),
4093
			'theme_supports' => 'custom-background',
4094
			'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4095
		) );
4096
4097
		$this->add_control( new WP_Customize_Background_Position_Control( $this, 'background_position', array(
4098
			'label'    => __( 'Image Position' ),
4099
			'section'  => 'background_image',
4100
			'settings' => array(
4101
				'x' => 'background_position_x',
4102
				'y' => 'background_position_y',
4103
			),
4104
		) ) );
4105
4106
		$this->add_setting( 'background_size', array(
4107
			'default'        => get_theme_support( 'custom-background', 'default-size' ),
4108
			'theme_supports' => 'custom-background',
4109
			'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4110
		) );
4111
4112
		$this->add_control( 'background_size', array(
4113
			'label'      => __( 'Image Size' ),
4114
			'section'    => 'background_image',
4115
			'type'       => 'select',
4116
			'choices'    => array(
4117
				'auto'    => __( 'Original' ),
4118
				'contain' => __( 'Fit to Screen' ),
4119
				'cover'   => __( 'Fill Screen' ),
4120
			),
4121
		) );
4122
4123
		$this->add_setting( 'background_repeat', array(
4124
			'default'           => get_theme_support( 'custom-background', 'default-repeat' ),
4125
			'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4126
			'theme_supports'    => 'custom-background',
4127
		) );
4128
4129
		$this->add_control( 'background_repeat', array(
4130
			'label'    => __( 'Repeat Background Image' ),
4131
			'section'  => 'background_image',
4132
			'type'     => 'checkbox',
4133
		) );
4134
4135
		$this->add_setting( 'background_attachment', array(
4136
			'default'           => get_theme_support( 'custom-background', 'default-attachment' ),
4137
			'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
4138
			'theme_supports'    => 'custom-background',
4139
		) );
4140
4141
		$this->add_control( 'background_attachment', array(
4142
			'label'    => __( 'Scroll with Page' ),
4143
			'section'  => 'background_image',
4144
			'type'     => 'checkbox',
4145
		) );
4146
4147
4148
		// If the theme is using the default background callback, we can update
4149
		// the background CSS using postMessage.
4150
		if ( get_theme_support( 'custom-background', 'wp-head-callback' ) === '_custom_background_cb' ) {
4151
			foreach ( array( 'color', 'image', 'preset', 'position_x', 'position_y', 'size', 'repeat', 'attachment' ) as $prop ) {
4152
				$this->get_setting( 'background_' . $prop )->transport = 'postMessage';
4153
			}
4154
		}
4155
4156
		/*
4157
		 * Static Front Page
4158
		 * See also https://core.trac.wordpress.org/ticket/19627 which introduces the the static-front-page theme_support.
4159
		 * The following replicates behavior from options-reading.php.
4160
		 */
4161
4162
		$this->add_section( 'static_front_page', array(
4163
			'title' => __( 'Static Front Page' ),
4164
			'priority' => 120,
4165
			'description' => __( 'Your theme supports a static front page.' ),
4166
			'active_callback' => array( $this, 'has_published_pages' ),
4167
		) );
4168
4169
		$this->add_setting( 'show_on_front', array(
4170
			'default' => get_option( 'show_on_front' ),
4171
			'capability' => 'manage_options',
4172
			'type' => 'option',
4173
		) );
4174
4175
		$this->add_control( 'show_on_front', array(
4176
			'label' => __( 'Front page displays' ),
4177
			'section' => 'static_front_page',
4178
			'type' => 'radio',
4179
			'choices' => array(
4180
				'posts' => __( 'Your latest posts' ),
4181
				'page'  => __( 'A static page' ),
4182
			),
4183
		) );
4184
4185
		$this->add_setting( 'page_on_front', array(
4186
			'type'       => 'option',
4187
			'capability' => 'manage_options',
4188
		) );
4189
4190
		$this->add_control( 'page_on_front', array(
4191
			'label' => __( 'Front page' ),
4192
			'section' => 'static_front_page',
4193
			'type' => 'dropdown-pages',
4194
			'allow_addition' => true,
4195
		) );
4196
4197
		$this->add_setting( 'page_for_posts', array(
4198
			'type' => 'option',
4199
			'capability' => 'manage_options',
4200
		) );
4201
4202
		$this->add_control( 'page_for_posts', array(
4203
			'label' => __( 'Posts page' ),
4204
			'section' => 'static_front_page',
4205
			'type' => 'dropdown-pages',
4206
			'allow_addition' => true,
4207
		) );
4208
4209
		/* Custom CSS */
4210
		$this->add_section( 'custom_css', array(
4211
			'title'              => __( 'Additional CSS' ),
4212
			'priority'           => 200,
4213
			'description_hidden' => true,
4214
			'description'        => sprintf( '%s<br /><a href="%s" class="external-link" target="_blank">%s<span class="screen-reader-text">%s</span></a>',
4215
				__( 'CSS allows you to customize the appearance and layout of your site with code. Separate CSS is saved for each of your themes. In the editing area the Tab key enters a tab character. To move below this area by pressing Tab, press the Esc key followed by the Tab key.' ),
4216
				esc_url( __( 'https://codex.wordpress.org/CSS' ) ),
4217
				__( 'Learn more about CSS' ),
4218
				/* translators: accessibility text */
4219
				__( '(opens in a new window)' )
4220
			),
4221
		) );
4222
4223
		$custom_css_setting = new WP_Customize_Custom_CSS_Setting( $this, sprintf( 'custom_css[%s]', get_stylesheet() ), array(
4224
			'capability' => 'edit_css',
4225
			'default' => sprintf( "/*\n%s\n*/", __( "You can add your own CSS here.\n\nClick the help icon above to learn more." ) ),
4226
		) );
4227
		$this->add_setting( $custom_css_setting );
4228
4229
		$this->add_control( 'custom_css', array(
4230
			'type'     => 'textarea',
4231
			'section'  => 'custom_css',
4232
			'settings' => array( 'default' => $custom_css_setting->id ),
4233
			'input_attrs' => array(
4234
				'class' => 'code', // Ensures contents displayed as LTR instead of RTL.
4235
			),
4236
		) );
4237
	}
4238
4239
	/**
4240
	 * Return whether there are published pages.
4241
	 *
4242
	 * Used as active callback for static front page section and controls.
4243
	 *
4244
	 * @since 4.7.0
4245
	 *
4246
	 * @returns bool Whether there are published (or to be published) pages.
4247
	 */
4248
	public function has_published_pages() {
4249
4250
		$setting = $this->get_setting( 'nav_menus_created_posts' );
4251
		if ( $setting ) {
4252
			foreach ( $setting->value() as $post_id ) {
4253
				if ( 'page' === get_post_type( $post_id ) ) {
4254
					return true;
4255
				}
4256
			}
4257
		}
4258
		return 0 !== count( get_pages() );
4259
	}
4260
4261
	/**
4262
	 * Add settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets
4263
	 *
4264
	 * @since 4.2.0
4265
	 * @access public
4266
	 *
4267
	 * @see add_dynamic_settings()
4268
	 */
4269
	public function register_dynamic_settings() {
4270
		$setting_ids = array_keys( $this->unsanitized_post_values() );
4271
		$this->add_dynamic_settings( $setting_ids );
4272
	}
4273
4274
	/**
4275
	 * Callback for validating the header_textcolor value.
4276
	 *
4277
	 * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
4278
	 * Returns default text color if hex color is empty.
4279
	 *
4280
	 * @since 3.4.0
4281
	 *
4282
	 * @param string $color
4283
	 * @return mixed
4284
	 */
4285
	public function _sanitize_header_textcolor( $color ) {
4286
		if ( 'blank' === $color )
4287
			return 'blank';
4288
4289
		$color = sanitize_hex_color_no_hash( $color );
4290
		if ( empty( $color ) )
4291
			$color = get_theme_support( 'custom-header', 'default-text-color' );
4292
4293
		return $color;
4294
	}
4295
4296
	/**
4297
	 * Callback for validating a background setting value.
4298
	 *
4299
	 * @since 4.7.0
4300
	 *
4301
	 * @param string $value Repeat value.
4302
	 * @param WP_Customize_Setting $setting Setting.
4303
	 * @return string|WP_Error Background value or validation error.
4304
	 */
4305
	public function _sanitize_background_setting( $value, $setting ) {
4306
		if ( 'background_repeat' === $setting->id ) {
4307
			if ( ! in_array( $value, array( 'repeat-x', 'repeat-y', 'repeat', 'no-repeat' ) ) ) {
4308
				return new WP_Error( 'invalid_value', __( 'Invalid value for background repeat.' ) );
4309
			}
4310 View Code Duplication
		} elseif ( 'background_attachment' === $setting->id ) {
4311
			if ( ! in_array( $value, array( 'fixed', 'scroll' ) ) ) {
4312
				return new WP_Error( 'invalid_value', __( 'Invalid value for background attachment.' ) );
4313
			}
4314
		} elseif ( 'background_position_x' === $setting->id ) {
4315
			if ( ! in_array( $value, array( 'left', 'center', 'right' ), true ) ) {
4316
				return new WP_Error( 'invalid_value', __( 'Invalid value for background position X.' ) );
4317
			}
4318 View Code Duplication
		} elseif ( 'background_position_y' === $setting->id ) {
4319
			if ( ! in_array( $value, array( 'top', 'center', 'bottom' ), true ) ) {
4320
				return new WP_Error( 'invalid_value', __( 'Invalid value for background position Y.' ) );
4321
			}
4322
		} elseif ( 'background_size' === $setting->id ) {
4323
			if ( ! in_array( $value, array( 'auto', 'contain', 'cover' ), true ) ) {
4324
				return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
4325
			}
4326
		} elseif ( 'background_preset' === $setting->id ) {
4327 View Code Duplication
			if ( ! in_array( $value, array( 'default', 'fill', 'fit', 'repeat', 'custom' ), true ) ) {
4328
				return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
4329
			}
4330
		} elseif ( 'background_image' === $setting->id || 'background_image_thumb' === $setting->id ) {
4331
			$value = empty( $value ) ? '' : esc_url_raw( $value );
4332
		} else {
4333
			return new WP_Error( 'unrecognized_setting', __( 'Unrecognized background setting.' ) );
4334
		}
4335
		return $value;
4336
	}
4337
4338
	/**
4339
	 * Export header video settings to facilitate selective refresh.
4340
	 *
4341
	 * @since 4.7.0
4342
	 *
4343
	 * @param array $response Response.
4344
	 * @param WP_Customize_Selective_Refresh $selective_refresh Selective refresh component.
4345
	 * @param array $partials Array of partials.
4346
	 * @return array
4347
	 */
4348
	public function export_header_video_settings( $response, $selective_refresh, $partials ) {
4349
		if ( isset( $partials['custom_header'] ) ) {
4350
			$response['custom_header_settings'] = get_header_video_settings();
4351
		}
4352
4353
		return $response;
4354
	}
4355
4356
	/**
4357
	 * Callback for validating the header_video value.
4358
	 *
4359
	 * Ensures that the selected video is less than 8MB and provides an error message.
4360
	 *
4361
	 * @since 4.7.0
4362
	 *
4363
	 * @param WP_Error $validity
4364
	 * @param mixed $value
4365
	 * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use WP_Error.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
4366
	 */
4367
	public function _validate_header_video( $validity, $value ) {
4368
		$video = get_attached_file( absint( $value ) );
4369
		if ( $video ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $video of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
4370
			$size = filesize( $video );
4371
			if ( 8 < $size / pow( 1024, 2 ) ) { // Check whether the size is larger than 8MB.
4372
				$validity->add( 'size_too_large',
4373
					__( 'This video file is too large to use as a header video. Try a shorter video or optimize the compression settings and re-upload a file that is less than 8MB. Or, upload your video to YouTube and link it with the option below.' )
4374
				);
4375
			}
4376
			if ( '.mp4' !== substr( $video, -4 ) && '.mov' !== substr( $video, -4 ) ) { // Check for .mp4 or .mov format, which (assuming h.264 encoding) are the only cross-browser-supported formats.
4377
				$validity->add( 'invalid_file_type', sprintf(
4378
					/* translators: 1: .mp4, 2: .mov */
4379
					__( 'Only %1$s or %2$s files may be used for header video. Please convert your video file and try again, or, upload your video to YouTube and link it with the option below.' ),
4380
					'<code>.mp4</code>',
4381
					'<code>.mov</code>'
4382
				) );
4383
			}
4384
		}
4385
		return $validity;
4386
	}
4387
4388
	/**
4389
	 * Callback for validating the external_header_video value.
4390
	 *
4391
	 * Ensures that the provided URL is supported.
4392
	 *
4393
	 * @since 4.7.0
4394
	 *
4395
	 * @param WP_Error $validity
4396
	 * @param mixed $value
4397
	 * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use WP_Error.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
4398
	 */
4399
	public function _validate_external_header_video( $validity, $value ) {
4400
		$video = esc_url_raw( $value );
4401
		if ( $video ) {
4402
			if ( ! preg_match( '#^https?://(?:www\.)?(?:youtube\.com/watch|youtu\.be/)#', $video ) ) {
4403
				$validity->add( 'invalid_url', __( 'Please enter a valid YouTube URL.' ) );
4404
			}
4405
		}
4406
		return $validity;
4407
	}
4408
4409
	/**
4410
	 * Callback for sanitizing the external_header_video value.
4411
	 *
4412
	 * @since 4.7.1
4413
	 *
4414
	 * @param string $value URL.
4415
	 * @return string Sanitized URL.
4416
	 */
4417
	public function _sanitize_external_header_video( $value ) {
4418
		return esc_url_raw( trim( $value ) );
4419
	}
4420
4421
	/**
4422
	 * Callback for rendering the custom logo, used in the custom_logo partial.
4423
	 *
4424
	 * This method exists because the partial object and context data are passed
4425
	 * into a partial's render_callback so we cannot use get_custom_logo() as
4426
	 * the render_callback directly since it expects a blog ID as the first
4427
	 * argument. When WP no longer supports PHP 5.3, this method can be removed
4428
	 * in favor of an anonymous function.
4429
	 *
4430
	 * @see WP_Customize_Manager::register_controls()
4431
	 *
4432
	 * @since 4.5.0
4433
	 *
4434
	 * @return string Custom logo.
4435
	 */
4436
	public function _render_custom_logo_partial() {
4437
		return get_custom_logo();
4438
	}
4439
}
4440