run:R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
23.8 KB
2026-03-11 16:18:51
R W Run
7.8 KB
2026-03-11 16:18:52
R W Run
36.1 KB
2026-03-11 16:18:51
R W Run
11.9 KB
2026-03-11 16:18:52
R W Run
18.94 KB
2026-03-11 16:18:52
R W Run
7.35 KB
2026-03-11 16:18:52
R W Run
28.6 KB
2026-03-11 16:18:51
R W Run
316 By
2026-03-11 16:18:51
R W Run
12.9 KB
2026-03-11 16:18:51
R W Run
61.02 KB
2026-03-11 16:18:52
R W Run
15 KB
2026-03-11 16:18:51
R W Run
112.05 KB
2026-03-11 16:18:51
R W Run
12.47 KB
2026-03-11 16:18:51
R W Run
15.07 KB
2026-03-11 16:18:52
R W Run
9.84 KB
2026-03-11 16:18:52
R W Run
13.17 KB
2026-03-11 16:18:52
R W Run
33.83 KB
2026-03-11 16:18:51
R W Run
42.63 KB
2026-03-11 16:18:51
R W Run
55.71 KB
2026-03-11 16:18:52
R W Run
12.53 KB
2026-03-11 16:18:51
R W Run
2.55 KB
2026-03-11 16:18:52
R W Run
28.92 KB
2026-03-11 16:18:52
R W Run
539 By
2026-03-11 16:18:51
R W Run
367 By
2026-03-11 16:18:52
R W Run
42.65 KB
2026-03-11 16:18:51
R W Run
401 By
2026-03-11 16:18:51
R W Run
6.61 KB
2026-03-11 16:18:51
R W Run
664 By
2026-03-11 16:18:52
R W Run
20.63 KB
2026-03-11 16:18:51
R W Run
2.18 KB
2026-03-11 16:18:52
R W Run
453 By
2026-03-11 16:18:52
R W Run
457 By
2026-03-11 16:18:51
R W Run
36.83 KB
2026-03-11 16:18:52
R W Run
2.41 KB
2026-03-11 16:18:52
R W Run
8.28 KB
2026-03-11 16:18:51
R W Run
13.89 KB
2026-03-11 16:18:51
R W Run
11.76 KB
2026-03-11 16:18:51
R W Run
2.65 KB
2026-03-11 16:18:51
R W Run
7.43 KB
2026-03-11 16:18:51
R W Run
17.46 KB
2026-03-11 16:18:51
R W Run
5.14 KB
2026-03-11 16:18:52
R W Run
16.7 KB
2026-03-11 16:18:51
R W Run
8.28 KB
2026-03-11 16:18:52
R W Run
2.92 KB
2026-03-11 16:18:52
R W Run
1.32 KB
2026-03-11 16:18:51
R W Run
4.6 KB
2026-03-11 16:18:52
R W Run
11.62 KB
2026-03-11 16:18:52
R W Run
2.5 KB
2026-03-11 16:18:51
R W Run
1.97 KB
2026-03-11 16:18:51
R W Run
11.25 KB
2026-03-11 16:18:52
R W Run
5.32 KB
2026-03-11 16:18:51
R W Run
10.99 KB
2026-03-11 16:18:52
R W Run
68.32 KB
2026-03-11 16:18:51
R W Run
6.34 KB
2026-03-11 16:18:51
R W Run
5.49 KB
2026-03-11 16:18:51
R W Run
1.99 KB
2026-03-11 16:18:52
R W Run
7.02 KB
2026-03-11 16:18:51
R W Run
4.91 KB
2026-03-11 16:18:52
R W Run
16.86 KB
2026-03-11 16:18:51
R W Run
24.23 KB
2026-03-11 16:18:51
R W Run
3.97 KB
2026-03-11 16:18:51
R W Run
47.66 KB
2026-03-11 16:18:51
R W Run
9.22 KB
2026-03-11 16:18:51
R W Run
25.51 KB
2026-03-11 16:18:51
R W Run
198.38 KB
2026-03-11 16:18:52
R W Run
56.65 KB
2026-03-11 16:18:51
R W Run
10.46 KB
2026-03-11 16:18:51
R W Run
10.95 KB
2026-03-11 16:18:52
R W Run
29.26 KB
2026-03-11 16:18:51
R W Run
70.91 KB
2026-03-11 16:18:52
R W Run
35.3 KB
2026-03-11 16:18:52
R W Run
16.61 KB
2026-03-11 16:18:52
R W Run
2.57 KB
2026-03-11 16:18:52
R W Run
39.83 KB
2026-03-11 16:18:51
R W Run
70.64 KB
2026-03-11 16:18:51
R W Run
15.56 KB
2026-03-11 16:18:52
R W Run
7.33 KB
2026-03-11 16:18:52
R W Run
253 By
2026-03-11 16:18:51
R W Run
7.96 KB
2026-03-11 16:18:52
R W Run
3.23 KB
2026-03-11 16:18:52
R W Run
969 By
2026-03-11 16:18:52
R W Run
16.28 KB
2026-03-11 16:18:51
R W Run
7.22 KB
2026-03-11 16:18:51
R W Run
12.95 KB
2026-03-11 16:18:51
R W Run
6.53 KB
2026-03-11 16:18:51
R W Run
3.42 KB
2026-03-11 16:18:52
R W Run
5.84 KB
2026-03-11 16:18:51
R W Run
1.97 KB
2026-03-11 16:18:51
R W Run
4.3 KB
2026-03-11 16:18:52
R W Run
2.91 KB
2026-03-11 16:18:51
R W Run
16.46 KB
2026-03-11 16:18:52
R W Run
40.6 KB
2026-03-11 16:18:51
R W Run
20.22 KB
2026-03-11 16:18:51
R W Run
36.11 KB
2026-03-11 16:18:52
R W Run
17.01 KB
2026-03-11 16:18:51
R W Run
7.27 KB
2026-03-11 16:18:52
R W Run
6.62 KB
2026-03-11 16:18:52
R W Run
16.49 KB
2026-03-11 16:18:52
R W Run
1.79 KB
2026-03-11 16:18:52
R W Run
29.82 KB
2026-03-11 16:18:51
R W Run
6.67 KB
2026-03-11 16:18:52
R W Run
8.98 KB
2026-03-11 16:18:52
R W Run
19.42 KB
2026-03-11 16:18:51
R W Run
12.01 KB
2026-03-11 16:18:51
R W Run
17.11 KB
2026-03-11 16:18:51
R W Run
6.74 KB
2026-03-11 16:18:52
R W Run
30.93 KB
2026-03-11 16:18:51
R W Run
4.99 KB
2026-03-11 16:18:51
R W Run
4.25 KB
2026-03-11 16:18:51
R W Run
24.72 KB
2026-03-11 16:18:51
R W Run
29.96 KB
2026-03-11 16:18:52
R W Run
6.41 KB
2026-03-11 16:18:51
R W Run
160 KB
2026-03-11 16:18:51
R W Run
6.72 KB
2026-03-11 16:18:52
R W Run
10.92 KB
2026-03-11 16:18:51
R W Run
4.77 KB
2026-03-11 16:18:51
R W Run
3.38 KB
2026-03-11 16:18:51
R W Run
11.18 KB
2026-03-11 16:18:51
R W Run
62.19 KB
2026-03-11 16:18:51
R W Run
2.46 KB
2026-03-11 16:18:51
R W Run
9.17 KB
2026-03-11 16:18:51
R W Run
32.15 KB
2026-03-11 16:18:51
R W Run
34.05 KB
2026-03-11 16:18:52
R W Run
7.15 KB
2026-03-11 16:18:51
R W Run
3.47 KB
2026-03-11 16:18:52
R W Run
1.87 KB
2026-03-11 16:18:52
R W Run
30.91 KB
2026-03-11 16:18:51
R W Run
7.29 KB
2026-03-11 16:18:52
R W Run
7.35 KB
2026-03-11 16:18:51
R W Run
12.54 KB
2026-03-11 16:18:51
R W Run
19.12 KB
2026-03-11 16:18:51
R W Run
18.12 KB
2026-03-11 16:18:52
R W Run
39.99 KB
2026-03-11 16:18:52
R W Run
5.17 KB
2026-03-11 16:18:52
R W Run
979 By
2026-03-11 16:18:51
R W Run
18.44 KB
2026-03-11 16:18:52
R W Run
10.24 KB
2026-03-11 16:18:51
R W Run
1.77 KB
2026-03-11 16:18:52
R W Run
34.9 KB
2026-03-11 16:18:51
R W Run
7.19 KB
2026-03-11 16:18:52
R W Run
160.5 KB
2026-03-11 16:18:51
R W Run
64.27 KB
2026-03-11 16:18:51
R W Run
27.95 KB
2026-03-11 16:18:51
R W Run
4.69 KB
2026-03-11 16:18:51
R W Run
2.94 KB
2026-03-11 16:18:51
R W Run
43.13 KB
2026-03-11 16:18:52
R W Run
2.25 KB
2026-03-11 16:18:52
R W Run
22.5 KB
2026-03-11 16:18:51
R W Run
13.01 KB
2026-03-11 16:18:52
R W Run
3.27 KB
2026-03-11 16:18:51
R W Run
18 KB
2026-03-11 16:18:51
R W Run
210.4 KB
2026-03-11 16:18:52
R W Run
25.86 KB
2026-03-11 16:18:52
R W Run
115.85 KB
2026-03-11 16:18:51
R W Run
373 By
2026-03-11 16:18:52
R W Run
343 By
2026-03-11 16:18:52
R W Run
338 By
2026-03-11 16:18:51
R W Run
100.73 KB
2026-03-11 16:18:52
R W Run
130.93 KB
2026-03-11 16:18:51
R W Run
19.1 KB
2026-03-11 16:18:51
R W Run
17.41 KB
2026-03-11 16:18:52
R W Run
41.98 KB
2026-03-11 16:18:52
R W Run
400 By
2026-03-11 16:18:52
R W Run
11.1 KB
2026-03-11 16:18:52
R W Run
37.02 KB
2026-03-11 16:18:51
R W Run
2.24 KB
2026-03-11 16:18:51
R W Run
188.13 KB
2026-03-11 16:18:51
R W Run
338 By
2026-03-11 16:18:51
R W Run
38 KB
2026-03-11 16:18:51
R W Run
4.02 KB
2026-03-11 16:18:52
R W Run
5.38 KB
2026-03-11 16:18:51
R W Run
3.05 KB
2026-03-11 16:18:52
R W Run
2.61 KB
2026-03-11 16:18:51
R W Run
1.16 KB
2026-03-11 16:18:52
R W Run
4.04 KB
2026-03-11 16:18:51
R W Run
3.71 KB
2026-03-11 16:18:51
R W Run
24.6 KB
2026-03-11 16:18:51
R W Run
9.56 KB
2026-03-11 16:18:51
R W Run
346.43 KB
2026-03-11 16:18:52
R W Run
281.84 KB
2026-03-11 16:18:52
R W Run
14.95 KB
2026-03-11 16:18:51
R W Run
8.44 KB
2026-03-11 16:18:52
R W Run
168.95 KB
2026-03-11 16:18:52
R W Run
20.71 KB
2026-03-11 16:18:52
R W Run
25.27 KB
2026-03-11 16:18:51
R W Run
5.72 KB
2026-03-11 16:18:51
R W Run
4.63 KB
2026-03-11 16:18:52
R W Run
81.73 KB
2026-03-11 16:18:51
R W Run
67.18 KB
2026-03-11 16:18:51
R W Run
156.36 KB
2026-03-11 16:18:52
R W Run
55.19 KB
2026-03-11 16:18:51
R W Run
162 By
2026-03-11 16:18:51
R W Run
61.72 KB
2026-03-11 16:18:51
R W Run
216.06 KB
2026-03-11 16:18:52
R W Run
65.09 KB
2026-03-11 16:18:51
R W Run
25.24 KB
2026-03-11 16:18:52
R W Run
4.81 KB
2026-03-11 16:18:51
R W Run
6.48 KB
2026-03-11 16:18:52
R W Run
21.25 KB
2026-03-11 16:18:51
R W Run
2.79 KB
2026-03-11 16:18:52
R W Run
89.69 KB
2026-03-11 16:18:52
R W Run
19.42 KB
2026-03-11 16:18:52
R W Run
3.69 KB
2026-03-11 16:18:52
R W Run
4.11 KB
2026-03-11 16:18:51
R W Run
40.74 KB
2026-03-11 16:18:51
R W Run
25.38 KB
2026-03-11 16:18:51
R W Run
43.31 KB
2026-03-11 16:18:52
R W Run
102.57 KB
2026-03-11 16:18:52
R W Run
6.18 KB
2026-03-11 16:18:51
R W Run
124.47 KB
2026-03-11 16:18:52
R W Run
35.65 KB
2026-03-11 16:18:52
R W Run
6.94 KB
2026-03-11 16:18:52
R W Run
67.04 KB
2026-03-11 16:18:52
R W Run
10.62 KB
2026-03-11 16:18:51
R W Run
289.35 KB
2026-03-11 16:18:52
R W Run
36.23 KB
2026-03-11 16:18:51
R W Run
200 By
2026-03-11 16:18:52
R W Run
200 By
2026-03-11 16:18:52
R W Run
98.29 KB
2026-03-11 16:18:52
R W Run
30.02 KB
2026-03-11 16:18:52
R W Run
19.03 KB
2026-03-11 16:18:52
R W Run
5.06 KB
2026-03-11 16:18:52
R W Run
255 By
2026-03-11 16:18:51
R W Run
22.66 KB
2026-03-11 16:18:52
R W Run
154.63 KB
2026-03-11 16:18:51
R W Run
9.68 KB
2026-03-11 16:18:51
R W Run
258 By
2026-03-11 16:18:51
R W Run
23.49 KB
2026-03-11 16:18:51
R W Run
3.16 KB
2026-03-11 16:18:51
R W Run
8.4 KB
2026-03-11 16:18:52
R W Run
441 By
2026-03-11 16:18:51
R W Run
7.39 KB
2026-03-11 16:18:51
R W Run
173 KB
2026-03-11 16:18:52
R W Run
544 By
2026-03-11 16:18:52
R W Run
4.17 KB
2026-03-11 16:18:51
R W Run
35.97 KB
2026-03-11 16:18:52
R W Run
1.69 KB
2026-03-11 16:18:51
R W Run
2.84 KB
2026-03-11 16:18:52
R W Run
6.09 KB
2026-03-11 16:18:51
R W Run
8.71 KB
2026-03-11 16:18:51
R W Run
131.84 KB
2026-03-11 16:18:51
R W Run
37.45 KB
2026-03-11 16:18:51
R W Run
173.89 KB
2026-03-11 16:18:51
R W Run
7.09 KB
2026-03-11 16:18:51
R W Run
6.41 KB
2026-03-11 16:18:51
R W Run
1.08 KB
2026-03-11 16:18:51
R W Run
69.46 KB
2026-03-11 16:18:52
R W Run
445 By
2026-03-11 16:18:51
R W Run
799 By
2026-03-11 16:18:52
R W Run
error_log
📄class-wp-customize-manager.php
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#[AllowDynamicProperties]
24final class WP_Customize_Manager {
25 /**
26 * An instance of the theme being previewed.
27 *
28 * @since 3.4.0
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 * @var string
38 */
39 protected $original_stylesheet;
40
41 /**
42 * Whether this is a Customizer pageload.
43 *
44 * @since 3.4.0
45 * @var bool
46 */
47 protected $previewing = false;
48
49 /**
50 * Methods and properties dealing with managing widgets in the Customizer.
51 *
52 * @since 3.9.0
53 * @var WP_Customize_Widgets
54 */
55 public $widgets;
56
57 /**
58 * Methods and properties dealing with managing nav menus in the Customizer.
59 *
60 * @since 4.3.0
61 * @var WP_Customize_Nav_Menus
62 */
63 public $nav_menus;
64
65 /**
66 * Methods and properties dealing with selective refresh in the Customizer preview.
67 *
68 * @since 4.5.0
69 * @var WP_Customize_Selective_Refresh
70 */
71 public $selective_refresh;
72
73 /**
74 * Registered instances of WP_Customize_Setting.
75 *
76 * @since 3.4.0
77 * @var array
78 */
79 protected $settings = array();
80
81 /**
82 * Sorted top-level instances of WP_Customize_Panel and WP_Customize_Section.
83 *
84 * @since 4.0.0
85 * @var array
86 */
87 protected $containers = array();
88
89 /**
90 * Registered instances of WP_Customize_Panel.
91 *
92 * @since 4.0.0
93 * @var array
94 */
95 protected $panels = array();
96
97 /**
98 * List of core components.
99 *
100 * @since 4.5.0
101 * @var array
102 */
103 protected $components = array( 'nav_menus' );
104
105 /**
106 * Registered instances of WP_Customize_Section.
107 *
108 * @since 3.4.0
109 * @var array
110 */
111 protected $sections = array();
112
113 /**
114 * Registered instances of WP_Customize_Control.
115 *
116 * @since 3.4.0
117 * @var array
118 */
119 protected $controls = array();
120
121 /**
122 * Panel types that may be rendered from JS templates.
123 *
124 * @since 4.3.0
125 * @var array
126 */
127 protected $registered_panel_types = array();
128
129 /**
130 * Section types that may be rendered from JS templates.
131 *
132 * @since 4.3.0
133 * @var array
134 */
135 protected $registered_section_types = array();
136
137 /**
138 * Control types that may be rendered from JS templates.
139 *
140 * @since 4.1.0
141 * @var array
142 */
143 protected $registered_control_types = array();
144
145 /**
146 * Initial URL being previewed.
147 *
148 * @since 4.4.0
149 * @var string
150 */
151 protected $preview_url;
152
153 /**
154 * URL to link the user to when closing the Customizer.
155 *
156 * @since 4.4.0
157 * @var string
158 */
159 protected $return_url;
160
161 /**
162 * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
163 *
164 * @since 4.4.0
165 * @var string[]
166 */
167 protected $autofocus = array();
168
169 /**
170 * Messenger channel.
171 *
172 * @since 4.7.0
173 * @var string
174 */
175 protected $messenger_channel;
176
177 /**
178 * Whether the autosave revision of the changeset should be loaded.
179 *
180 * @since 4.9.0
181 * @var bool
182 */
183 protected $autosaved = false;
184
185 /**
186 * Whether the changeset branching is allowed.
187 *
188 * @since 4.9.0
189 * @var bool
190 */
191 protected $branching = true;
192
193 /**
194 * Whether settings should be previewed.
195 *
196 * @since 4.9.0
197 * @var bool
198 */
199 protected $settings_previewed = true;
200
201 /**
202 * Whether a starter content changeset was saved.
203 *
204 * @since 4.9.0
205 * @var bool
206 */
207 protected $saved_starter_content_changeset = false;
208
209 /**
210 * Unsanitized values for Customize Settings parsed from $_POST['customized'].
211 *
212 * @var array
213 */
214 private $_post_values;
215
216 /**
217 * Changeset UUID.
218 *
219 * @since 4.7.0
220 * @var string
221 */
222 private $_changeset_uuid;
223
224 /**
225 * Changeset post ID.
226 *
227 * @since 4.7.0
228 * @var int|false
229 */
230 private $_changeset_post_id;
231
232 /**
233 * Changeset data loaded from a customize_changeset post.
234 *
235 * @since 4.7.0
236 * @var array|null
237 */
238 private $_changeset_data;
239
240 /**
241 * Constructor.
242 *
243 * @since 3.4.0
244 * @since 4.7.0 Added `$args` parameter.
245 *
246 * @param array $args {
247 * Args.
248 *
249 * @type null|string|false $changeset_uuid Changeset UUID, the `post_name` for the customize_changeset post containing the customized state.
250 * Defaults to `null` resulting in a UUID to be immediately generated. If `false` is provided, then
251 * then the changeset UUID will be determined during `after_setup_theme`: when the
252 * `customize_changeset_branching` filter returns false, then the default UUID will be that
253 * of the most recent `customize_changeset` post that has a status other than 'auto-draft',
254 * 'publish', or 'trash'. Otherwise, if changeset branching is enabled, then a random UUID will be used.
255 * @type string $theme Theme to be previewed (for theme switch). Defaults to customize_theme or theme query params.
256 * @type string $messenger_channel Messenger channel. Defaults to customize_messenger_channel query param.
257 * @type bool $settings_previewed If settings should be previewed. Defaults to true.
258 * @type bool $branching If changeset branching is allowed; otherwise, changesets are linear. Defaults to true.
259 * @type bool $autosaved If data from a changeset's autosaved revision should be loaded if it exists. Defaults to false.
260 * }
261 */
262 public function __construct( $args = array() ) {
263
264 $args = array_merge(
265 array_fill_keys( array( 'changeset_uuid', 'theme', 'messenger_channel', 'settings_previewed', 'autosaved', 'branching' ), null ),
266 $args
267 );
268
269 // Note that the UUID format will be validated in the setup_theme() method.
270 if ( ! isset( $args['changeset_uuid'] ) ) {
271 $args['changeset_uuid'] = wp_generate_uuid4();
272 }
273
274 /*
275 * The theme and messenger_channel should be supplied via $args,
276 * but they are also looked at in the $_REQUEST global here for back-compat.
277 */
278 if ( ! isset( $args['theme'] ) ) {
279 if ( isset( $_REQUEST['customize_theme'] ) ) {
280 $args['theme'] = wp_unslash( $_REQUEST['customize_theme'] );
281 } elseif ( isset( $_REQUEST['theme'] ) ) { // Deprecated.
282 $args['theme'] = wp_unslash( $_REQUEST['theme'] );
283 }
284 }
285 if ( ! isset( $args['messenger_channel'] ) && isset( $_REQUEST['customize_messenger_channel'] ) ) {
286 $args['messenger_channel'] = sanitize_key( wp_unslash( $_REQUEST['customize_messenger_channel'] ) );
287 }
288
289 // Do not load 'widgets' component if a block theme is activated.
290 if ( ! wp_is_block_theme() ) {
291 $this->components[] = 'widgets';
292 }
293
294 $this->original_stylesheet = get_stylesheet();
295 $this->theme = wp_get_theme( 0 === validate_file( $args['theme'] ) ? $args['theme'] : null );
296 $this->messenger_channel = $args['messenger_channel'];
297 $this->_changeset_uuid = $args['changeset_uuid'];
298
299 foreach ( array( 'settings_previewed', 'autosaved', 'branching' ) as $key ) {
300 if ( isset( $args[ $key ] ) ) {
301 $this->$key = (bool) $args[ $key ];
302 }
303 }
304
305 require_once ABSPATH . WPINC . '/class-wp-customize-setting.php';
306 require_once ABSPATH . WPINC . '/class-wp-customize-panel.php';
307 require_once ABSPATH . WPINC . '/class-wp-customize-section.php';
308 require_once ABSPATH . WPINC . '/class-wp-customize-control.php';
309
310 require_once ABSPATH . WPINC . '/customize/class-wp-customize-color-control.php';
311 require_once ABSPATH . WPINC . '/customize/class-wp-customize-media-control.php';
312 require_once ABSPATH . WPINC . '/customize/class-wp-customize-upload-control.php';
313 require_once ABSPATH . WPINC . '/customize/class-wp-customize-image-control.php';
314 require_once ABSPATH . WPINC . '/customize/class-wp-customize-background-image-control.php';
315 require_once ABSPATH . WPINC . '/customize/class-wp-customize-background-position-control.php';
316 require_once ABSPATH . WPINC . '/customize/class-wp-customize-cropped-image-control.php';
317 require_once ABSPATH . WPINC . '/customize/class-wp-customize-site-icon-control.php';
318 require_once ABSPATH . WPINC . '/customize/class-wp-customize-header-image-control.php';
319 require_once ABSPATH . WPINC . '/customize/class-wp-customize-theme-control.php';
320 require_once ABSPATH . WPINC . '/customize/class-wp-customize-code-editor-control.php';
321 require_once ABSPATH . WPINC . '/customize/class-wp-widget-area-customize-control.php';
322 require_once ABSPATH . WPINC . '/customize/class-wp-widget-form-customize-control.php';
323 require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-control.php';
324 require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-control.php';
325 require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-location-control.php';
326 require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-name-control.php';
327 require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-locations-control.php';
328 require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-auto-add-control.php';
329
330 require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menus-panel.php';
331
332 require_once ABSPATH . WPINC . '/customize/class-wp-customize-themes-panel.php';
333 require_once ABSPATH . WPINC . '/customize/class-wp-customize-themes-section.php';
334 require_once ABSPATH . WPINC . '/customize/class-wp-customize-sidebar-section.php';
335 require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-section.php';
336
337 require_once ABSPATH . WPINC . '/customize/class-wp-customize-custom-css-setting.php';
338 require_once ABSPATH . WPINC . '/customize/class-wp-customize-filter-setting.php';
339 require_once ABSPATH . WPINC . '/customize/class-wp-customize-header-image-setting.php';
340 require_once ABSPATH . WPINC . '/customize/class-wp-customize-background-image-setting.php';
341 require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-setting.php';
342 require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-setting.php';
343
344 /**
345 * Filters the core Customizer components to load.
346 *
347 * This allows Core components to be excluded from being instantiated by
348 * filtering them out of the array. Note that this filter generally runs
349 * during the {@see 'plugins_loaded'} action, so it cannot be added
350 * in a theme.
351 *
352 * @since 4.4.0
353 *
354 * @see WP_Customize_Manager::__construct()
355 *
356 * @param string[] $components Array of core components to load.
357 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
358 */
359 $components = apply_filters( 'customize_loaded_components', $this->components, $this );
360
361 require_once ABSPATH . WPINC . '/customize/class-wp-customize-selective-refresh.php';
362 $this->selective_refresh = new WP_Customize_Selective_Refresh( $this );
363
364 if ( in_array( 'widgets', $components, true ) ) {
365 require_once ABSPATH . WPINC . '/class-wp-customize-widgets.php';
366 $this->widgets = new WP_Customize_Widgets( $this );
367 }
368
369 if ( in_array( 'nav_menus', $components, true ) ) {
370 require_once ABSPATH . WPINC . '/class-wp-customize-nav-menus.php';
371 $this->nav_menus = new WP_Customize_Nav_Menus( $this );
372 }
373
374 add_action( 'setup_theme', array( $this, 'setup_theme' ) );
375 add_action( 'wp_loaded', array( $this, 'wp_loaded' ) );
376
377 // Do not spawn cron (especially the alternate cron) while running the Customizer.
378 remove_action( 'init', 'wp_cron' );
379
380 // Do not run update checks when rendering the controls.
381 remove_action( 'admin_init', '_maybe_update_core' );
382 remove_action( 'admin_init', '_maybe_update_plugins' );
383 remove_action( 'admin_init', '_maybe_update_themes' );
384
385 add_action( 'wp_ajax_customize_save', array( $this, 'save' ) );
386 add_action( 'wp_ajax_customize_trash', array( $this, 'handle_changeset_trash_request' ) );
387 add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
388 add_action( 'wp_ajax_customize_load_themes', array( $this, 'handle_load_themes_request' ) );
389 add_filter( 'heartbeat_settings', array( $this, 'add_customize_screen_to_heartbeat_settings' ) );
390 add_filter( 'heartbeat_received', array( $this, 'check_changeset_lock_with_heartbeat' ), 10, 3 );
391 add_action( 'wp_ajax_customize_override_changeset_lock', array( $this, 'handle_override_changeset_lock_request' ) );
392 add_action( 'wp_ajax_customize_dismiss_autosave_or_lock', array( $this, 'handle_dismiss_autosave_or_lock_request' ) );
393
394 add_action( 'customize_register', array( $this, 'register_controls' ) );
395 add_action( 'customize_register', array( $this, 'register_dynamic_settings' ), 11 ); // Allow code to create settings first.
396 add_action( 'customize_controls_init', array( $this, 'prepare_controls' ) );
397 add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
398
399 // Render Common, Panel, Section, and Control templates.
400 add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_panel_templates' ), 1 );
401 add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_section_templates' ), 1 );
402 add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_control_templates' ), 1 );
403
404 // Export header video settings with the partial response.
405 add_filter( 'customize_render_partials_response', array( $this, 'export_header_video_settings' ), 10, 3 );
406
407 // Export the settings to JS via the _wpCustomizeSettings variable.
408 add_action( 'customize_controls_print_footer_scripts', array( $this, 'customize_pane_settings' ), 1000 );
409
410 // Add theme update notices.
411 if ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) ) {
412 require_once ABSPATH . 'wp-admin/includes/update.php';
413 add_action( 'customize_controls_print_footer_scripts', 'wp_print_admin_notice_templates' );
414 }
415 }
416
417 /**
418 * Returns true if it's an Ajax request.
419 *
420 * @since 3.4.0
421 * @since 4.2.0 Added `$action` param.
422 *
423 * @param string|null $action Whether the supplied Ajax action is being run.
424 * @return bool True if it's an Ajax request, false otherwise.
425 */
426 public function doing_ajax( $action = null ) {
427 if ( ! wp_doing_ajax() ) {
428 return false;
429 }
430
431 if ( ! $action ) {
432 return true;
433 } else {
434 /*
435 * Note: we can't just use doing_action( "wp_ajax_{$action}" ) because we need
436 * to check before admin-ajax.php gets to that point.
437 */
438 return isset( $_REQUEST['action'] ) && wp_unslash( $_REQUEST['action'] ) === $action;
439 }
440 }
441
442 /**
443 * Custom wp_die wrapper. Returns either the standard message for UI
444 * or the Ajax message.
445 *
446 * @since 3.4.0
447 *
448 * @param string|WP_Error $ajax_message Ajax return.
449 * @param string $message Optional. UI message.
450 */
451 protected function wp_die( $ajax_message, $message = null ) {
452 if ( $this->doing_ajax() ) {
453 wp_die( $ajax_message );
454 }
455
456 if ( ! $message ) {
457 $message = __( 'An error occurred while customizing. Please refresh the page and try again.' );
458 }
459
460 if ( $this->messenger_channel ) {
461 ob_start();
462 wp_enqueue_scripts();
463 wp_print_scripts( array( 'customize-base' ) );
464
465 $settings = array(
466 'messengerArgs' => array(
467 'channel' => $this->messenger_channel,
468 'url' => wp_customize_url(),
469 ),
470 'error' => $ajax_message,
471 );
472 $message .= ob_get_clean();
473 ob_start();
474 ?>
475 <script>
476 ( function( api, settings ) {
477 var preview = new api.Messenger( settings.messengerArgs );
478 preview.send( 'iframe-loading-error', settings.error );
479 } )( wp.customize, <?php echo wp_json_encode( $settings, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ); ?> );
480 </script>
481 <?php
482 $message .= wp_get_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) . "\n//# sourceURL=" . rawurlencode( __METHOD__ ) );
483 }
484
485 wp_die( $message );
486 }
487
488 /**
489 * Returns the Ajax wp_die() handler if it's a customized request.
490 *
491 * @since 3.4.0
492 * @deprecated 4.7.0
493 *
494 * @return callable Die handler.
495 */
496 public function wp_die_handler() {
497 _deprecated_function( __METHOD__, '4.7.0' );
498
499 if ( $this->doing_ajax() || isset( $_POST['customized'] ) ) {
500 return '_ajax_wp_die_handler';
501 }
502
503 return '_default_wp_die_handler';
504 }
505
506 /**
507 * Starts preview and customize theme.
508 *
509 * Check if customize query variable exist. Init filters to filter the active theme.
510 *
511 * @since 3.4.0
512 *
513 * @global string $pagenow The filename of the current screen.
514 */
515 public function setup_theme() {
516 global $pagenow;
517
518 // Check permissions for customize.php access since this method is called before customize.php can run any code.
519 if ( 'customize.php' === $pagenow && ! current_user_can( 'customize' ) ) {
520 if ( ! is_user_logged_in() ) {
521 auth_redirect();
522 } else {
523 wp_die(
524 '<h1>' . __( 'You need a higher level of permission.' ) . '</h1>' .
525 '<p>' . __( 'Sorry, you are not allowed to customize this site.' ) . '</p>',
526 403
527 );
528 }
529 return;
530 }
531
532 // If a changeset was provided is invalid.
533 if ( isset( $this->_changeset_uuid ) && false !== $this->_changeset_uuid && ! wp_is_uuid( $this->_changeset_uuid ) ) {
534 $this->wp_die( -1, __( 'Invalid changeset UUID' ) );
535 }
536
537 /*
538 * Clear incoming post data if the user lacks a CSRF token (nonce). Note that the customizer
539 * application will inject the customize_preview_nonce query parameter into all Ajax requests.
540 * For similar behavior elsewhere in WordPress, see rest_cookie_check_errors() which logs out
541 * a user when a valid nonce isn't present.
542 */
543 $has_post_data_nonce = (
544 check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'nonce', false )
545 ||
546 check_ajax_referer( 'save-customize_' . $this->get_stylesheet(), 'nonce', false )
547 ||
548 check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'customize_preview_nonce', false )
549 );
550 if ( ! current_user_can( 'customize' ) || ! $has_post_data_nonce ) {
551 unset( $_POST['customized'] );
552 unset( $_REQUEST['customized'] );
553 }
554
555 /*
556 * If unauthenticated then require a valid changeset UUID to load the preview.
557 * In this way, the UUID serves as a secret key. If the messenger channel is present,
558 * then send unauthenticated code to prompt re-auth.
559 */
560 if ( ! current_user_can( 'customize' ) && ! $this->changeset_post_id() ) {
561 $this->wp_die( $this->messenger_channel ? 0 : -1, __( 'Non-existent changeset UUID.' ) );
562 }
563
564 if ( ! headers_sent() ) {
565 send_origin_headers();
566 }
567
568 // Hide the admin bar if we're embedded in the customizer iframe.
569 if ( $this->messenger_channel ) {
570 show_admin_bar( false );
571 }
572
573 if ( $this->is_theme_active() ) {
574 // Once the theme is loaded, we'll validate it.
575 add_action( 'after_setup_theme', array( $this, 'after_setup_theme' ) );
576 } else {
577 /*
578 * If the requested theme is not the active theme and the user doesn't have
579 * the switch_themes cap, bail.
580 */
581 if ( ! current_user_can( 'switch_themes' ) ) {
582 $this->wp_die( -1, __( 'Sorry, you are not allowed to edit theme options on this site.' ) );
583 }
584
585 // If the theme has errors while loading, bail.
586 if ( $this->theme()->errors() ) {
587 $this->wp_die( -1, $this->theme()->errors()->get_error_message() );
588 }
589
590 // If the theme isn't allowed per multisite settings, bail.
591 if ( ! $this->theme()->is_allowed() ) {
592 $this->wp_die( -1, __( 'The requested theme does not exist.' ) );
593 }
594 }
595
596 // Make sure changeset UUID is established immediately after the theme is loaded.
597 add_action( 'after_setup_theme', array( $this, 'establish_loaded_changeset' ), 5 );
598
599 /*
600 * Import theme starter content for fresh installations when landing in the customizer.
601 * Import starter content at after_setup_theme:100 so that any
602 * add_theme_support( 'starter-content' ) calls will have been made.
603 */
604 if ( get_option( 'fresh_site' ) && 'customize.php' === $pagenow ) {
605 add_action( 'after_setup_theme', array( $this, 'import_theme_starter_content' ), 100 );
606 }
607
608 $this->start_previewing_theme();
609 }
610
611 /**
612 * Establishes the loaded changeset.
613 *
614 * This method runs right at after_setup_theme and applies the 'customize_changeset_branching' filter to determine
615 * whether concurrent changesets are allowed. Then if the Customizer is not initialized with a `changeset_uuid` param,
616 * this method will determine which UUID should be used. If changeset branching is disabled, then the most saved
617 * changeset will be loaded by default. Otherwise, if there are no existing saved changesets or if changeset branching is
618 * enabled, then a new UUID will be generated.
619 *
620 * @since 4.9.0
621 *
622 * @global string $pagenow The filename of the current screen.
623 */
624 public function establish_loaded_changeset() {
625 global $pagenow;
626
627 if ( empty( $this->_changeset_uuid ) ) {
628 $changeset_uuid = null;
629
630 if ( ! $this->branching() && $this->is_theme_active() ) {
631 $unpublished_changeset_posts = $this->get_changeset_posts(
632 array(
633 'post_status' => array_diff( get_post_stati(), array( 'auto-draft', 'publish', 'trash', 'inherit', 'private' ) ),
634 'exclude_restore_dismissed' => false,
635 'author' => 'any',
636 'posts_per_page' => 1,
637 'order' => 'DESC',
638 'orderby' => 'date',
639 )
640 );
641 $unpublished_changeset_post = array_shift( $unpublished_changeset_posts );
642 if ( ! empty( $unpublished_changeset_post ) && wp_is_uuid( $unpublished_changeset_post->post_name ) ) {
643 $changeset_uuid = $unpublished_changeset_post->post_name;
644 }
645 }
646
647 // If no changeset UUID has been set yet, then generate a new one.
648 if ( empty( $changeset_uuid ) ) {
649 $changeset_uuid = wp_generate_uuid4();
650 }
651
652 $this->_changeset_uuid = $changeset_uuid;
653 }
654
655 if ( is_admin() && 'customize.php' === $pagenow ) {
656 $this->set_changeset_lock( $this->changeset_post_id() );
657 }
658 }
659
660 /**
661 * Callback to validate a theme once it is loaded
662 *
663 * @since 3.4.0
664 */
665 public function after_setup_theme() {
666 $doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_POST['customized'] ) );
667 if ( ! $doing_ajax_or_is_customized && ! validate_current_theme() ) {
668 wp_redirect( 'themes.php?broken=true' );
669 exit;
670 }
671 }
672
673 /**
674 * If the theme to be previewed isn't the active theme, add filter callbacks
675 * to swap it out at runtime.
676 *
677 * @since 3.4.0
678 */
679 public function start_previewing_theme() {
680 // Bail if we're already previewing.
681 if ( $this->is_preview() ) {
682 return;
683 }
684
685 $this->previewing = true;
686
687 if ( ! $this->is_theme_active() ) {
688 add_filter( 'template', array( $this, 'get_template' ) );
689 add_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
690 add_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
691
692 // @link: https://core.trac.wordpress.org/ticket/20027
693 add_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
694 add_filter( 'pre_option_template', array( $this, 'get_template' ) );
695
696 // Handle custom theme roots.
697 add_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
698 add_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
699 }
700
701 /**
702 * Fires once the Customizer theme preview has started.
703 *
704 * @since 3.4.0
705 *
706 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
707 */
708 do_action( 'start_previewing_theme', $this );
709 }
710
711 /**
712 * Stops previewing the selected theme.
713 *
714 * Removes filters to change the active theme.
715 *
716 * @since 3.4.0
717 */
718 public function stop_previewing_theme() {
719 if ( ! $this->is_preview() ) {
720 return;
721 }
722
723 $this->previewing = false;
724
725 if ( ! $this->is_theme_active() ) {
726 remove_filter( 'template', array( $this, 'get_template' ) );
727 remove_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
728 remove_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
729
730 // @link: https://core.trac.wordpress.org/ticket/20027
731 remove_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
732 remove_filter( 'pre_option_template', array( $this, 'get_template' ) );
733
734 // Handle custom theme roots.
735 remove_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
736 remove_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
737 }
738
739 /**
740 * Fires once the Customizer theme preview has stopped.
741 *
742 * @since 3.4.0
743 *
744 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
745 */
746 do_action( 'stop_previewing_theme', $this );
747 }
748
749 /**
750 * Gets whether settings are or will be previewed.
751 *
752 * @since 4.9.0
753 *
754 * @see WP_Customize_Setting::preview()
755 *
756 * @return bool
757 */
758 public function settings_previewed() {
759 return $this->settings_previewed;
760 }
761
762 /**
763 * Gets whether data from a changeset's autosaved revision should be loaded if it exists.
764 *
765 * @since 4.9.0
766 *
767 * @see WP_Customize_Manager::changeset_data()
768 *
769 * @return bool Is using autosaved changeset revision.
770 */
771 public function autosaved() {
772 return $this->autosaved;
773 }
774
775 /**
776 * Whether the changeset branching is allowed.
777 *
778 * @since 4.9.0
779 *
780 * @see WP_Customize_Manager::establish_loaded_changeset()
781 *
782 * @return bool Is changeset branching.
783 */
784 public function branching() {
785
786 /**
787 * Filters whether or not changeset branching is allowed.
788 *
789 * By default in core, when changeset branching is not allowed, changesets will operate
790 * linearly in that only one saved changeset will exist at a time (with a 'draft' or
791 * 'future' status). This makes the Customizer operate in a way that is similar to going to
792 * "edit" to one existing post: all users will be making changes to the same post, and autosave
793 * revisions will be made for that post.
794 *
795 * By contrast, when changeset branching is allowed, then the model is like users going
796 * to "add new" for a page and each user makes changes independently of each other since
797 * they are all operating on their own separate pages, each getting their own separate
798 * initial auto-drafts and then once initially saved, autosave revisions on top of that
799 * user's specific post.
800 *
801 * Since linear changesets are deemed to be more suitable for the majority of WordPress users,
802 * they are the default. For WordPress sites that have heavy site management in the Customizer
803 * by multiple users then branching changesets should be enabled by means of this filter.
804 *
805 * @since 4.9.0
806 *
807 * @param bool $allow_branching Whether branching is allowed. If `false`, the default,
808 * then only one saved changeset exists at a time.
809 * @param WP_Customize_Manager $wp_customize Manager instance.
810 */
811 $this->branching = apply_filters( 'customize_changeset_branching', $this->branching, $this );
812
813 return $this->branching;
814 }
815
816 /**
817 * Gets the changeset UUID.
818 *
819 * @since 4.7.0
820 *
821 * @see WP_Customize_Manager::establish_loaded_changeset()
822 *
823 * @return string UUID.
824 */
825 public function changeset_uuid() {
826 if ( empty( $this->_changeset_uuid ) ) {
827 $this->establish_loaded_changeset();
828 }
829 return $this->_changeset_uuid;
830 }
831
832 /**
833 * Gets the theme being customized.
834 *
835 * @since 3.4.0
836 *
837 * @return WP_Theme
838 */
839 public function theme() {
840 if ( ! $this->theme ) {
841 $this->theme = wp_get_theme();
842 }
843 return $this->theme;
844 }
845
846 /**
847 * Gets the registered settings.
848 *
849 * @since 3.4.0
850 *
851 * @return array
852 */
853 public function settings() {
854 return $this->settings;
855 }
856
857 /**
858 * Gets the registered controls.
859 *
860 * @since 3.4.0
861 *
862 * @return array
863 */
864 public function controls() {
865 return $this->controls;
866 }
867
868 /**
869 * Gets the registered containers.
870 *
871 * @since 4.0.0
872 *
873 * @return array
874 */
875 public function containers() {
876 return $this->containers;
877 }
878
879 /**
880 * Gets the registered sections.
881 *
882 * @since 3.4.0
883 *
884 * @return array
885 */
886 public function sections() {
887 return $this->sections;
888 }
889
890 /**
891 * Gets the registered panels.
892 *
893 * @since 4.0.0
894 *
895 * @return array Panels.
896 */
897 public function panels() {
898 return $this->panels;
899 }
900
901 /**
902 * Checks if the current theme is active.
903 *
904 * @since 3.4.0
905 *
906 * @return bool
907 */
908 public function is_theme_active() {
909 return $this->get_stylesheet() === $this->original_stylesheet;
910 }
911
912 /**
913 * Registers styles/scripts and initialize the preview of each setting
914 *
915 * @since 3.4.0
916 */
917 public function wp_loaded() {
918
919 /*
920 * Unconditionally register core types for panels, sections, and controls
921 * in case plugin unhooks all customize_register actions.
922 */
923 $this->register_panel_type( 'WP_Customize_Panel' );
924 $this->register_panel_type( 'WP_Customize_Themes_Panel' );
925 $this->register_section_type( 'WP_Customize_Section' );
926 $this->register_section_type( 'WP_Customize_Sidebar_Section' );
927 $this->register_section_type( 'WP_Customize_Themes_Section' );
928 $this->register_control_type( 'WP_Customize_Color_Control' );
929 $this->register_control_type( 'WP_Customize_Media_Control' );
930 $this->register_control_type( 'WP_Customize_Upload_Control' );
931 $this->register_control_type( 'WP_Customize_Image_Control' );
932 $this->register_control_type( 'WP_Customize_Background_Image_Control' );
933 $this->register_control_type( 'WP_Customize_Background_Position_Control' );
934 $this->register_control_type( 'WP_Customize_Cropped_Image_Control' );
935 $this->register_control_type( 'WP_Customize_Site_Icon_Control' );
936 $this->register_control_type( 'WP_Customize_Theme_Control' );
937 $this->register_control_type( 'WP_Customize_Code_Editor_Control' );
938 $this->register_control_type( 'WP_Customize_Date_Time_Control' );
939
940 /**
941 * Fires once WordPress has loaded, allowing scripts and styles to be initialized.
942 *
943 * @since 3.4.0
944 *
945 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
946 */
947 do_action( 'customize_register', $this );
948
949 if ( $this->settings_previewed() ) {
950 foreach ( $this->settings as $setting ) {
951 $setting->preview();
952 }
953 }
954
955 if ( $this->is_preview() && ! is_admin() ) {
956 $this->customize_preview_init();
957 }
958 }
959
960 /**
961 * Prevents Ajax requests from following redirects when previewing a theme
962 * by issuing a 200 response instead of a 30x.
963 *
964 * Instead, the JS will sniff out the location header.
965 *
966 * @since 3.4.0
967 * @deprecated 4.7.0
968 *
969 * @param int $status Status.
970 * @return int
971 */
972 public function wp_redirect_status( $status ) {
973 _deprecated_function( __FUNCTION__, '4.7.0' );
974
975 if ( $this->is_preview() && ! is_admin() ) {
976 return 200;
977 }
978
979 return $status;
980 }
981
982 /**
983 * Finds the changeset post ID for a given changeset UUID.
984 *
985 * @since 4.7.0
986 *
987 * @param string $uuid Changeset UUID.
988 * @return int|null Returns post ID on success and null on failure.
989 */
990 public function find_changeset_post_id( $uuid ) {
991 $cache_group = 'customize_changeset_post';
992 $changeset_post_id = wp_cache_get( $uuid, $cache_group );
993 if ( $changeset_post_id && 'customize_changeset' === get_post_type( $changeset_post_id ) ) {
994 return $changeset_post_id;
995 }
996
997 $changeset_post_query = new WP_Query(
998 array(
999 'post_type' => 'customize_changeset',
1000 'post_status' => get_post_stati(),
1001 'name' => $uuid,
1002 'posts_per_page' => 1,
1003 'no_found_rows' => true,
1004 'cache_results' => true,
1005 'update_post_meta_cache' => false,
1006 'update_post_term_cache' => false,
1007 'lazy_load_term_meta' => false,
1008 )
1009 );
1010 if ( ! empty( $changeset_post_query->posts ) ) {
1011 // Note: 'fields'=>'ids' is not being used in order to cache the post object as it will be needed.
1012 $changeset_post_id = $changeset_post_query->posts[0]->ID;
1013 wp_cache_set( $uuid, $changeset_post_id, $cache_group );
1014 return $changeset_post_id;
1015 }
1016
1017 return null;
1018 }
1019
1020 /**
1021 * Gets changeset posts.
1022 *
1023 * @since 4.9.0
1024 *
1025 * @param array $args {
1026 * Args to pass into `get_posts()` to query changesets.
1027 *
1028 * @type int $posts_per_page Number of posts to return. Defaults to -1 (all posts).
1029 * @type int $author Post author. Defaults to current user.
1030 * @type string $post_status Status of changeset. Defaults to 'auto-draft'.
1031 * @type bool $exclude_restore_dismissed Whether to exclude changeset auto-drafts that have been dismissed. Defaults to true.
1032 * }
1033 * @return WP_Post[] Auto-draft changesets.
1034 */
1035 protected function get_changeset_posts( $args = array() ) {
1036 $default_args = array(
1037 'exclude_restore_dismissed' => true,
1038 'posts_per_page' => -1,
1039 'post_type' => 'customize_changeset',
1040 'post_status' => 'auto-draft',
1041 'order' => 'DESC',
1042 'orderby' => 'date',
1043 'no_found_rows' => true,
1044 'cache_results' => true,
1045 'update_post_meta_cache' => false,
1046 'update_post_term_cache' => false,
1047 'lazy_load_term_meta' => false,
1048 );
1049 if ( get_current_user_id() ) {
1050 $default_args['author'] = get_current_user_id();
1051 }
1052 $args = array_merge( $default_args, $args );
1053
1054 if ( ! empty( $args['exclude_restore_dismissed'] ) ) {
1055 unset( $args['exclude_restore_dismissed'] );
1056 $args['meta_query'] = array(
1057 array(
1058 'key' => '_customize_restore_dismissed',
1059 'compare' => 'NOT EXISTS',
1060 ),
1061 );
1062 }
1063
1064 return get_posts( $args );
1065 }
1066
1067 /**
1068 * Dismisses all of the current user's auto-drafts (other than the present one).
1069 *
1070 * @since 4.9.0
1071 * @return int The number of auto-drafts that were dismissed.
1072 */
1073 protected function dismiss_user_auto_draft_changesets() {
1074 $changeset_autodraft_posts = $this->get_changeset_posts(
1075 array(
1076 'post_status' => 'auto-draft',
1077 'exclude_restore_dismissed' => true,
1078 'posts_per_page' => -1,
1079 )
1080 );
1081 $dismissed = 0;
1082 foreach ( $changeset_autodraft_posts as $autosave_autodraft_post ) {
1083 if ( $autosave_autodraft_post->ID === $this->changeset_post_id() ) {
1084 continue;
1085 }
1086 if ( update_post_meta( $autosave_autodraft_post->ID, '_customize_restore_dismissed', true ) ) {
1087 ++$dismissed;
1088 }
1089 }
1090 return $dismissed;
1091 }
1092
1093 /**
1094 * Gets the changeset post ID for the loaded changeset.
1095 *
1096 * @since 4.7.0
1097 *
1098 * @return int|null Post ID on success or null if there is no post yet saved.
1099 */
1100 public function changeset_post_id() {
1101 if ( ! isset( $this->_changeset_post_id ) ) {
1102 $post_id = $this->find_changeset_post_id( $this->changeset_uuid() );
1103 if ( ! $post_id ) {
1104 $post_id = false;
1105 }
1106 $this->_changeset_post_id = $post_id;
1107 }
1108 if ( false === $this->_changeset_post_id ) {
1109 return null;
1110 }
1111 return $this->_changeset_post_id;
1112 }
1113
1114 /**
1115 * Gets the data stored in a changeset post.
1116 *
1117 * @since 4.7.0
1118 *
1119 * @param int $post_id Changeset post ID.
1120 * @return array|WP_Error Changeset data or WP_Error on error.
1121 */
1122 protected function get_changeset_post_data( $post_id ) {
1123 if ( ! $post_id ) {
1124 return new WP_Error( 'empty_post_id' );
1125 }
1126 $changeset_post = get_post( $post_id );
1127 if ( ! $changeset_post ) {
1128 return new WP_Error( 'missing_post' );
1129 }
1130 if ( 'revision' === $changeset_post->post_type ) {
1131 if ( 'customize_changeset' !== get_post_type( $changeset_post->post_parent ) ) {
1132 return new WP_Error( 'wrong_post_type' );
1133 }
1134 } elseif ( 'customize_changeset' !== $changeset_post->post_type ) {
1135 return new WP_Error( 'wrong_post_type' );
1136 }
1137 $changeset_data = json_decode( $changeset_post->post_content, true );
1138 $last_error = json_last_error();
1139 if ( $last_error ) {
1140 return new WP_Error( 'json_parse_error', '', $last_error );
1141 }
1142 if ( ! is_array( $changeset_data ) ) {
1143 return new WP_Error( 'expected_array' );
1144 }
1145 return $changeset_data;
1146 }
1147
1148 /**
1149 * Gets changeset data.
1150 *
1151 * @since 4.7.0
1152 * @since 4.9.0 This will return the changeset's data with a user's autosave revision merged on top, if one exists and $autosaved is true.
1153 *
1154 * @return array Changeset data.
1155 */
1156 public function changeset_data() {
1157 if ( isset( $this->_changeset_data ) ) {
1158 return $this->_changeset_data;
1159 }
1160 $changeset_post_id = $this->changeset_post_id();
1161 if ( ! $changeset_post_id ) {
1162 $this->_changeset_data = array();
1163 } else {
1164 if ( $this->autosaved() && is_user_logged_in() ) {
1165 $autosave_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
1166 if ( $autosave_post ) {
1167 $data = $this->get_changeset_post_data( $autosave_post->ID );
1168 if ( ! is_wp_error( $data ) ) {
1169 $this->_changeset_data = $data;
1170 }
1171 }
1172 }
1173
1174 // Load data from the changeset if it was not loaded from an autosave.
1175 if ( ! isset( $this->_changeset_data ) ) {
1176 $data = $this->get_changeset_post_data( $changeset_post_id );
1177 if ( ! is_wp_error( $data ) ) {
1178 $this->_changeset_data = $data;
1179 } else {
1180 $this->_changeset_data = array();
1181 }
1182 }
1183 }
1184 return $this->_changeset_data;
1185 }
1186
1187 /**
1188 * Starter content setting IDs.
1189 *
1190 * @since 4.7.0
1191 * @var array
1192 */
1193 protected $pending_starter_content_settings_ids = array();
1194
1195 /**
1196 * Imports theme starter content into the customized state.
1197 *
1198 * @since 4.7.0
1199 *
1200 * @param array $starter_content Starter content. Defaults to `get_theme_starter_content()`.
1201 */
1202 public function import_theme_starter_content( $starter_content = array() ) {
1203 if ( empty( $starter_content ) ) {
1204 $starter_content = get_theme_starter_content();
1205 }
1206
1207 $changeset_data = array();
1208 if ( $this->changeset_post_id() ) {
1209 /*
1210 * Don't re-import starter content into a changeset saved persistently.
1211 * This will need to be revisited in the future once theme switching
1212 * is allowed with drafted/scheduled changesets, since switching to
1213 * another theme could result in more starter content being applied.
1214 * However, when doing an explicit save it is currently possible for
1215 * nav menus and nav menu items specifically to lose their starter_content
1216 * flags, thus resulting in duplicates being created since they fail
1217 * to get re-used. See #40146.
1218 */
1219 if ( 'auto-draft' !== get_post_status( $this->changeset_post_id() ) ) {
1220 return;
1221 }
1222
1223 $changeset_data = $this->get_changeset_post_data( $this->changeset_post_id() );
1224 }
1225
1226 $sidebars_widgets = isset( $starter_content['widgets'] ) && ! empty( $this->widgets ) ? $starter_content['widgets'] : array();
1227 $attachments = isset( $starter_content['attachments'] ) && ! empty( $this->nav_menus ) ? $starter_content['attachments'] : array();
1228 $posts = isset( $starter_content['posts'] ) && ! empty( $this->nav_menus ) ? $starter_content['posts'] : array();
1229 $options = isset( $starter_content['options'] ) ? $starter_content['options'] : array();
1230 $nav_menus = isset( $starter_content['nav_menus'] ) && ! empty( $this->nav_menus ) ? $starter_content['nav_menus'] : array();
1231 $theme_mods = isset( $starter_content['theme_mods'] ) ? $starter_content['theme_mods'] : array();
1232
1233 // Widgets.
1234 $max_widget_numbers = array();
1235 foreach ( $sidebars_widgets as $sidebar_id => $widgets ) {
1236 $sidebar_widget_ids = array();
1237 foreach ( $widgets as $widget ) {
1238 list( $id_base, $instance ) = $widget;
1239
1240 if ( ! isset( $max_widget_numbers[ $id_base ] ) ) {
1241
1242 // When $settings is an array-like object, get an intrinsic array for use with array_keys().
1243 $settings = get_option( "widget_{$id_base}", array() );
1244 if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) {
1245 $settings = $settings->getArrayCopy();
1246 }
1247
1248 unset( $settings['_multiwidget'] );
1249
1250 // Find the max widget number for this type.
1251 $widget_numbers = array_keys( $settings );
1252 if ( count( $widget_numbers ) > 0 ) {
1253 $widget_numbers[] = 1;
1254 $max_widget_numbers[ $id_base ] = max( ...$widget_numbers );
1255 } else {
1256 $max_widget_numbers[ $id_base ] = 1;
1257 }
1258 }
1259 $max_widget_numbers[ $id_base ] += 1;
1260
1261 $widget_id = sprintf( '%s-%d', $id_base, $max_widget_numbers[ $id_base ] );
1262 $setting_id = sprintf( 'widget_%s[%d]', $id_base, $max_widget_numbers[ $id_base ] );
1263
1264 $setting_value = $this->widgets->sanitize_widget_js_instance( $instance );
1265 if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
1266 $this->set_post_value( $setting_id, $setting_value );
1267 $this->pending_starter_content_settings_ids[] = $setting_id;
1268 }
1269 $sidebar_widget_ids[] = $widget_id;
1270 }
1271
1272 $setting_id = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
1273 if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
1274 $this->set_post_value( $setting_id, $sidebar_widget_ids );
1275 $this->pending_starter_content_settings_ids[] = $setting_id;
1276 }
1277 }
1278
1279 $starter_content_auto_draft_post_ids = array();
1280 if ( ! empty( $changeset_data['nav_menus_created_posts']['value'] ) ) {
1281 $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, $changeset_data['nav_menus_created_posts']['value'] );
1282 }
1283
1284 // Make an index of all the posts needed and what their slugs are.
1285 $needed_posts = array();
1286 $attachments = $this->prepare_starter_content_attachments( $attachments );
1287 foreach ( $attachments as $attachment ) {
1288 $key = 'attachment:' . $attachment['post_name'];
1289 $needed_posts[ $key ] = true;
1290 }
1291 foreach ( array_keys( $posts ) as $post_symbol ) {
1292 if ( empty( $posts[ $post_symbol ]['post_name'] ) && empty( $posts[ $post_symbol ]['post_title'] ) ) {
1293 unset( $posts[ $post_symbol ] );
1294 continue;
1295 }
1296 if ( empty( $posts[ $post_symbol ]['post_name'] ) ) {
1297 $posts[ $post_symbol ]['post_name'] = sanitize_title( $posts[ $post_symbol ]['post_title'] );
1298 }
1299 if ( empty( $posts[ $post_symbol ]['post_type'] ) ) {
1300 $posts[ $post_symbol ]['post_type'] = 'post';
1301 }
1302 $needed_posts[ $posts[ $post_symbol ]['post_type'] . ':' . $posts[ $post_symbol ]['post_name'] ] = true;
1303 }
1304 $all_post_slugs = array_merge(
1305 wp_list_pluck( $attachments, 'post_name' ),
1306 wp_list_pluck( $posts, 'post_name' )
1307 );
1308
1309 /*
1310 * Obtain all post types referenced in starter content to use in query.
1311 * This is needed because 'any' will not account for post types not yet registered.
1312 */
1313 $post_types = array_filter( array_merge( array( 'attachment' ), wp_list_pluck( $posts, 'post_type' ) ) );
1314
1315 // Re-use auto-draft starter content posts referenced in the current customized state.
1316 $existing_starter_content_posts = array();
1317 if ( ! empty( $starter_content_auto_draft_post_ids ) ) {
1318 $existing_posts_query = new WP_Query(
1319 array(
1320 'post__in' => $starter_content_auto_draft_post_ids,
1321 'post_status' => 'auto-draft',
1322 'post_type' => $post_types,
1323 'posts_per_page' => -1,
1324 )
1325 );
1326 foreach ( $existing_posts_query->posts as $existing_post ) {
1327 $post_name = $existing_post->post_name;
1328 if ( empty( $post_name ) ) {
1329 $post_name = get_post_meta( $existing_post->ID, '_customize_draft_post_name', true );
1330 }
1331 $existing_starter_content_posts[ $existing_post->post_type . ':' . $post_name ] = $existing_post;
1332 }
1333 }
1334
1335 // Re-use non-auto-draft posts.
1336 if ( ! empty( $all_post_slugs ) ) {
1337 $existing_posts_query = new WP_Query(
1338 array(
1339 'post_name__in' => $all_post_slugs,
1340 'post_status' => array_diff( get_post_stati(), array( 'auto-draft' ) ),
1341 'post_type' => 'any',
1342 'posts_per_page' => -1,
1343 )
1344 );
1345 foreach ( $existing_posts_query->posts as $existing_post ) {
1346 $key = $existing_post->post_type . ':' . $existing_post->post_name;
1347 if ( isset( $needed_posts[ $key ] ) && ! isset( $existing_starter_content_posts[ $key ] ) ) {
1348 $existing_starter_content_posts[ $key ] = $existing_post;
1349 }
1350 }
1351 }
1352
1353 // Attachments are technically posts but handled differently.
1354 if ( ! empty( $attachments ) ) {
1355
1356 $attachment_ids = array();
1357
1358 foreach ( $attachments as $symbol => $attachment ) {
1359 $file_array = array(
1360 'name' => $attachment['file_name'],
1361 );
1362 $file_path = $attachment['file_path'];
1363 $attachment_id = null;
1364 $attached_file = null;
1365 if ( isset( $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ] ) ) {
1366 $attachment_post = $existing_starter_content_posts[ 'attachment:' . $attachment['post_name'] ];
1367 $attachment_id = $attachment_post->ID;
1368 $attached_file = get_attached_file( $attachment_id );
1369 if ( empty( $attached_file ) || ! file_exists( $attached_file ) ) {
1370 $attachment_id = null;
1371 $attached_file = null;
1372 } elseif ( $this->get_stylesheet() !== get_post_meta( $attachment_post->ID, '_starter_content_theme', true ) ) {
1373
1374 // Re-generate attachment metadata since it was previously generated for a different theme.
1375 $metadata = wp_generate_attachment_metadata( $attachment_post->ID, $attached_file );
1376 wp_update_attachment_metadata( $attachment_id, $metadata );
1377 update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
1378 }
1379 }
1380
1381 // Insert the attachment auto-draft because it doesn't yet exist or the attached file is gone.
1382 if ( ! $attachment_id ) {
1383
1384 // Copy file to temp location so that original file won't get deleted from theme after sideloading.
1385 $temp_file_name = wp_tempnam( wp_basename( $file_path ) );
1386 if ( $temp_file_name && copy( $file_path, $temp_file_name ) ) {
1387 $file_array['tmp_name'] = $temp_file_name;
1388 }
1389 if ( empty( $file_array['tmp_name'] ) ) {
1390 continue;
1391 }
1392
1393 $attachment_post_data = array_merge(
1394 wp_array_slice_assoc( $attachment, array( 'post_title', 'post_content', 'post_excerpt' ) ),
1395 array(
1396 'post_status' => 'auto-draft', // So attachment will be garbage collected in a week if changeset is never published.
1397 )
1398 );
1399
1400 $attachment_id = media_handle_sideload( $file_array, 0, null, $attachment_post_data );
1401 if ( is_wp_error( $attachment_id ) ) {
1402 continue;
1403 }
1404 update_post_meta( $attachment_id, '_starter_content_theme', $this->get_stylesheet() );
1405 update_post_meta( $attachment_id, '_customize_draft_post_name', $attachment['post_name'] );
1406 }
1407
1408 $attachment_ids[ $symbol ] = $attachment_id;
1409 }
1410 $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, array_values( $attachment_ids ) );
1411 }
1412
1413 // Posts & pages.
1414 if ( ! empty( $posts ) ) {
1415 foreach ( array_keys( $posts ) as $post_symbol ) {
1416 if ( empty( $posts[ $post_symbol ]['post_type'] ) || empty( $posts[ $post_symbol ]['post_name'] ) ) {
1417 continue;
1418 }
1419 $post_type = $posts[ $post_symbol ]['post_type'];
1420 if ( ! empty( $posts[ $post_symbol ]['post_name'] ) ) {
1421 $post_name = $posts[ $post_symbol ]['post_name'];
1422 } elseif ( ! empty( $posts[ $post_symbol ]['post_title'] ) ) {
1423 $post_name = sanitize_title( $posts[ $post_symbol ]['post_title'] );
1424 } else {
1425 continue;
1426 }
1427
1428 // Use existing auto-draft post if one already exists with the same type and name.
1429 if ( isset( $existing_starter_content_posts[ $post_type . ':' . $post_name ] ) ) {
1430 $posts[ $post_symbol ]['ID'] = $existing_starter_content_posts[ $post_type . ':' . $post_name ]->ID;
1431 continue;
1432 }
1433
1434 // Translate the featured image symbol.
1435 if ( ! empty( $posts[ $post_symbol ]['thumbnail'] )
1436 && preg_match( '/^{{(?P<symbol>.+)}}$/', $posts[ $post_symbol ]['thumbnail'], $matches )
1437 && isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1438 $posts[ $post_symbol ]['meta_input']['_thumbnail_id'] = $attachment_ids[ $matches['symbol'] ];
1439 }
1440
1441 if ( ! empty( $posts[ $post_symbol ]['template'] ) ) {
1442 $posts[ $post_symbol ]['meta_input']['_wp_page_template'] = $posts[ $post_symbol ]['template'];
1443 }
1444
1445 $r = $this->nav_menus->insert_auto_draft_post( $posts[ $post_symbol ] );
1446 if ( $r instanceof WP_Post ) {
1447 $posts[ $post_symbol ]['ID'] = $r->ID;
1448 }
1449 }
1450
1451 $starter_content_auto_draft_post_ids = array_merge( $starter_content_auto_draft_post_ids, wp_list_pluck( $posts, 'ID' ) );
1452 }
1453
1454 // The nav_menus_created_posts setting is why nav_menus component is dependency for adding posts.
1455 if ( ! empty( $this->nav_menus ) && ! empty( $starter_content_auto_draft_post_ids ) ) {
1456 $setting_id = 'nav_menus_created_posts';
1457 $this->set_post_value( $setting_id, array_unique( array_values( $starter_content_auto_draft_post_ids ) ) );
1458 $this->pending_starter_content_settings_ids[] = $setting_id;
1459 }
1460
1461 // Nav menus.
1462 $placeholder_id = -1;
1463 $reused_nav_menu_setting_ids = array();
1464 foreach ( $nav_menus as $nav_menu_location => $nav_menu ) {
1465
1466 $nav_menu_term_id = null;
1467 $nav_menu_setting_id = null;
1468 $matches = array();
1469
1470 // Look for an existing placeholder menu with starter content to re-use.
1471 foreach ( $changeset_data as $setting_id => $setting_params ) {
1472 $can_reuse = (
1473 ! empty( $setting_params['starter_content'] )
1474 &&
1475 ! in_array( $setting_id, $reused_nav_menu_setting_ids, true )
1476 &&
1477 preg_match( '#^nav_menu\[(?P<nav_menu_id>-?\d+)\]$#', $setting_id, $matches )
1478 );
1479 if ( $can_reuse ) {
1480 $nav_menu_term_id = (int) $matches['nav_menu_id'];
1481 $nav_menu_setting_id = $setting_id;
1482 $reused_nav_menu_setting_ids[] = $setting_id;
1483 break;
1484 }
1485 }
1486
1487 if ( ! $nav_menu_term_id ) {
1488 while ( isset( $changeset_data[ sprintf( 'nav_menu[%d]', $placeholder_id ) ] ) ) {
1489 --$placeholder_id;
1490 }
1491 $nav_menu_term_id = $placeholder_id;
1492 $nav_menu_setting_id = sprintf( 'nav_menu[%d]', $placeholder_id );
1493 }
1494
1495 $this->set_post_value(
1496 $nav_menu_setting_id,
1497 array(
1498 'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location,
1499 )
1500 );
1501 $this->pending_starter_content_settings_ids[] = $nav_menu_setting_id;
1502
1503 // @todo Add support for menu_item_parent.
1504 $position = 0;
1505 foreach ( $nav_menu['items'] as $nav_menu_item ) {
1506 $nav_menu_item_setting_id = sprintf( 'nav_menu_item[%d]', $placeholder_id-- );
1507 if ( ! isset( $nav_menu_item['position'] ) ) {
1508 $nav_menu_item['position'] = $position++;
1509 }
1510 $nav_menu_item['nav_menu_term_id'] = $nav_menu_term_id;
1511
1512 if ( isset( $nav_menu_item['object_id'] ) ) {
1513 if ( 'post_type' === $nav_menu_item['type'] && preg_match( '/^{{(?P<symbol>.+)}}$/', $nav_menu_item['object_id'], $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
1514 $nav_menu_item['object_id'] = $posts[ $matches['symbol'] ]['ID'];
1515 if ( empty( $nav_menu_item['title'] ) ) {
1516 $original_object = get_post( $nav_menu_item['object_id'] );
1517 $nav_menu_item['title'] = $original_object->post_title;
1518 }
1519 } else {
1520 continue;
1521 }
1522 } else {
1523 $nav_menu_item['object_id'] = 0;
1524 }
1525
1526 if ( empty( $changeset_data[ $nav_menu_item_setting_id ] ) || ! empty( $changeset_data[ $nav_menu_item_setting_id ]['starter_content'] ) ) {
1527 $this->set_post_value( $nav_menu_item_setting_id, $nav_menu_item );
1528 $this->pending_starter_content_settings_ids[] = $nav_menu_item_setting_id;
1529 }
1530 }
1531
1532 $setting_id = sprintf( 'nav_menu_locations[%s]', $nav_menu_location );
1533 if ( empty( $changeset_data[ $setting_id ] ) || ! empty( $changeset_data[ $setting_id ]['starter_content'] ) ) {
1534 $this->set_post_value( $setting_id, $nav_menu_term_id );
1535 $this->pending_starter_content_settings_ids[] = $setting_id;
1536 }
1537 }
1538
1539 // Options.
1540 foreach ( $options as $name => $value ) {
1541
1542 // Serialize the value to check for post symbols.
1543 $value = maybe_serialize( $value );
1544
1545 if ( is_serialized( $value ) ) {
1546 if ( preg_match( '/s:\d+:"{{(?P<symbol>.+)}}"/', $value, $matches ) ) {
1547 if ( isset( $posts[ $matches['symbol'] ] ) ) {
1548 $symbol_match = $posts[ $matches['symbol'] ]['ID'];
1549 } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1550 $symbol_match = $attachment_ids[ $matches['symbol'] ];
1551 }
1552
1553 // If we have any symbol matches, update the values.
1554 if ( isset( $symbol_match ) ) {
1555 // Replace found string matches with post IDs.
1556 $value = str_replace( $matches[0], "i:{$symbol_match}", $value );
1557 } else {
1558 continue;
1559 }
1560 }
1561 } elseif ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
1562 if ( isset( $posts[ $matches['symbol'] ] ) ) {
1563 $value = $posts[ $matches['symbol'] ]['ID'];
1564 } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1565 $value = $attachment_ids[ $matches['symbol'] ];
1566 } else {
1567 continue;
1568 }
1569 }
1570
1571 // Unserialize values after checking for post symbols, so they can be properly referenced.
1572 $value = maybe_unserialize( $value );
1573
1574 if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
1575 $this->set_post_value( $name, $value );
1576 $this->pending_starter_content_settings_ids[] = $name;
1577 }
1578 }
1579
1580 // Theme mods.
1581 foreach ( $theme_mods as $name => $value ) {
1582
1583 // Serialize the value to check for post symbols.
1584 $value = maybe_serialize( $value );
1585
1586 // Check if value was serialized.
1587 if ( is_serialized( $value ) ) {
1588 if ( preg_match( '/s:\d+:"{{(?P<symbol>.+)}}"/', $value, $matches ) ) {
1589 if ( isset( $posts[ $matches['symbol'] ] ) ) {
1590 $symbol_match = $posts[ $matches['symbol'] ]['ID'];
1591 } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1592 $symbol_match = $attachment_ids[ $matches['symbol'] ];
1593 }
1594
1595 // If we have any symbol matches, update the values.
1596 if ( isset( $symbol_match ) ) {
1597 // Replace found string matches with post IDs.
1598 $value = str_replace( $matches[0], "i:{$symbol_match}", $value );
1599 } else {
1600 continue;
1601 }
1602 }
1603 } elseif ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) ) {
1604 if ( isset( $posts[ $matches['symbol'] ] ) ) {
1605 $value = $posts[ $matches['symbol'] ]['ID'];
1606 } elseif ( isset( $attachment_ids[ $matches['symbol'] ] ) ) {
1607 $value = $attachment_ids[ $matches['symbol'] ];
1608 } else {
1609 continue;
1610 }
1611 }
1612
1613 // Unserialize values after checking for post symbols, so they can be properly referenced.
1614 $value = maybe_unserialize( $value );
1615
1616 // Handle header image as special case since setting has a legacy format.
1617 if ( 'header_image' === $name ) {
1618 $name = 'header_image_data';
1619 $metadata = wp_get_attachment_metadata( $value );
1620 if ( empty( $metadata ) ) {
1621 continue;
1622 }
1623 $value = array(
1624 'attachment_id' => $value,
1625 'url' => wp_get_attachment_url( $value ),
1626 'height' => $metadata['height'],
1627 'width' => $metadata['width'],
1628 );
1629 } elseif ( 'background_image' === $name ) {
1630 $value = wp_get_attachment_url( $value );
1631 }
1632
1633 if ( empty( $changeset_data[ $name ] ) || ! empty( $changeset_data[ $name ]['starter_content'] ) ) {
1634 $this->set_post_value( $name, $value );
1635 $this->pending_starter_content_settings_ids[] = $name;
1636 }
1637 }
1638
1639 if ( ! empty( $this->pending_starter_content_settings_ids ) ) {
1640 if ( did_action( 'customize_register' ) ) {
1641 $this->_save_starter_content_changeset();
1642 } else {
1643 add_action( 'customize_register', array( $this, '_save_starter_content_changeset' ), 1000 );
1644 }
1645 }
1646 }
1647
1648 /**
1649 * Prepares starter content attachments.
1650 *
1651 * Ensure that the attachments are valid and that they have slugs and file name/path.
1652 *
1653 * @since 4.7.0
1654 *
1655 * @param array $attachments Attachments.
1656 * @return array Prepared attachments.
1657 */
1658 protected function prepare_starter_content_attachments( $attachments ) {
1659 $prepared_attachments = array();
1660 if ( empty( $attachments ) ) {
1661 return $prepared_attachments;
1662 }
1663
1664 // Such is The WordPress Way.
1665 require_once ABSPATH . 'wp-admin/includes/file.php';
1666 require_once ABSPATH . 'wp-admin/includes/media.php';
1667 require_once ABSPATH . 'wp-admin/includes/image.php';
1668
1669 foreach ( $attachments as $symbol => $attachment ) {
1670
1671 // A file is required and URLs to files are not currently allowed.
1672 if ( empty( $attachment['file'] ) || preg_match( '#^https?://$#', $attachment['file'] ) ) {
1673 continue;
1674 }
1675
1676 $file_path = null;
1677 if ( file_exists( $attachment['file'] ) ) {
1678 $file_path = $attachment['file']; // Could be absolute path to file in plugin.
1679 } elseif ( is_child_theme() && file_exists( get_stylesheet_directory() . '/' . $attachment['file'] ) ) {
1680 $file_path = get_stylesheet_directory() . '/' . $attachment['file'];
1681 } elseif ( file_exists( get_template_directory() . '/' . $attachment['file'] ) ) {
1682 $file_path = get_template_directory() . '/' . $attachment['file'];
1683 } else {
1684 continue;
1685 }
1686 $file_name = wp_basename( $attachment['file'] );
1687
1688 // Skip file types that are not recognized.
1689 $checked_filetype = wp_check_filetype( $file_name );
1690 if ( empty( $checked_filetype['type'] ) ) {
1691 continue;
1692 }
1693
1694 // Ensure post_name is set since not automatically derived from post_title for new auto-draft posts.
1695 if ( empty( $attachment['post_name'] ) ) {
1696 if ( ! empty( $attachment['post_title'] ) ) {
1697 $attachment['post_name'] = sanitize_title( $attachment['post_title'] );
1698 } else {
1699 $attachment['post_name'] = sanitize_title( preg_replace( '/\.\w+$/', '', $file_name ) );
1700 }
1701 }
1702
1703 $attachment['file_name'] = $file_name;
1704 $attachment['file_path'] = $file_path;
1705 $prepared_attachments[ $symbol ] = $attachment;
1706 }
1707 return $prepared_attachments;
1708 }
1709
1710 /**
1711 * Saves starter content changeset.
1712 *
1713 * @since 4.7.0
1714 */
1715 public function _save_starter_content_changeset() {
1716
1717 if ( empty( $this->pending_starter_content_settings_ids ) ) {
1718 return;
1719 }
1720
1721 $this->save_changeset_post(
1722 array(
1723 'data' => array_fill_keys( $this->pending_starter_content_settings_ids, array( 'starter_content' => true ) ),
1724 'starter_content' => true,
1725 )
1726 );
1727 $this->saved_starter_content_changeset = true;
1728
1729 $this->pending_starter_content_settings_ids = array();
1730 }
1731
1732 /**
1733 * Gets dirty pre-sanitized setting values in the current customized state.
1734 *
1735 * The returned array consists of a merge of three sources:
1736 * 1. If the theme is not currently active, then the base array is any stashed
1737 * theme mods that were modified previously but never published.
1738 * 2. The values from the current changeset, if it exists.
1739 * 3. If the user can customize, the values parsed from the incoming
1740 * `$_POST['customized']` JSON data.
1741 * 4. Any programmatically-set post values via `WP_Customize_Manager::set_post_value()`.
1742 *
1743 * The name "unsanitized_post_values" is a carry-over from when the customized
1744 * state was exclusively sourced from `$_POST['customized']`. Nevertheless,
1745 * the value returned will come from the current changeset post and from the
1746 * incoming post data.
1747 *
1748 * @since 4.1.1
1749 * @since 4.7.0 Added `$args` parameter and merging with changeset values and stashed theme mods.
1750 *
1751 * @param array $args {
1752 * Args.
1753 *
1754 * @type bool $exclude_changeset Whether the changeset values should also be excluded. Defaults to false.
1755 * @type bool $exclude_post_data Whether the post input values should also be excluded. Defaults to false when lacking the customize capability.
1756 * }
1757 * @return array
1758 */
1759 public function unsanitized_post_values( $args = array() ) {
1760 $args = array_merge(
1761 array(
1762 'exclude_changeset' => false,
1763 'exclude_post_data' => ! current_user_can( 'customize' ),
1764 ),
1765 $args
1766 );
1767
1768 $values = array();
1769
1770 // Let default values be from the stashed theme mods if doing a theme switch and if no changeset is present.
1771 if ( ! $this->is_theme_active() ) {
1772 $stashed_theme_mods = get_option( 'customize_stashed_theme_mods' );
1773 $stylesheet = $this->get_stylesheet();
1774 if ( isset( $stashed_theme_mods[ $stylesheet ] ) ) {
1775 $values = array_merge( $values, wp_list_pluck( $stashed_theme_mods[ $stylesheet ], 'value' ) );
1776 }
1777 }
1778
1779 if ( ! $args['exclude_changeset'] ) {
1780 foreach ( $this->changeset_data() as $setting_id => $setting_params ) {
1781 if ( ! array_key_exists( 'value', $setting_params ) ) {
1782 continue;
1783 }
1784 if ( isset( $setting_params['type'] ) && 'theme_mod' === $setting_params['type'] ) {
1785
1786 // Ensure that theme mods values are only used if they were saved under the active theme.
1787 $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
1788 if ( preg_match( $namespace_pattern, $setting_id, $matches ) && $this->get_stylesheet() === $matches['stylesheet'] ) {
1789 $values[ $matches['setting_id'] ] = $setting_params['value'];
1790 }
1791 } else {
1792 $values[ $setting_id ] = $setting_params['value'];
1793 }
1794 }
1795 }
1796
1797 if ( ! $args['exclude_post_data'] ) {
1798 if ( ! isset( $this->_post_values ) ) {
1799 if ( isset( $_POST['customized'] ) ) {
1800 $post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
1801 } else {
1802 $post_values = array();
1803 }
1804 if ( is_array( $post_values ) ) {
1805 $this->_post_values = $post_values;
1806 } else {
1807 $this->_post_values = array();
1808 }
1809 }
1810 $values = array_merge( $values, $this->_post_values );
1811 }
1812 return $values;
1813 }
1814
1815 /**
1816 * Returns the sanitized value for a given setting from the current customized state.
1817 *
1818 * The name "post_value" is a carry-over from when the customized state was exclusively
1819 * sourced from `$_POST['customized']`. Nevertheless, the value returned will come
1820 * from the current changeset post and from the incoming post data.
1821 *
1822 * @since 3.4.0
1823 * @since 4.1.1 Introduced the `$default_value` parameter.
1824 * @since 4.6.0 `$default_value` is now returned early when the setting post value is invalid.
1825 *
1826 * @see WP_REST_Server::dispatch()
1827 * @see WP_REST_Request::sanitize_params()
1828 * @see WP_REST_Request::has_valid_params()
1829 *
1830 * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object.
1831 * @param mixed $default_value Value returned if `$setting` has no post value (added in 4.2.0)
1832 * or the post value is invalid (added in 4.6.0).
1833 * @return string|mixed Sanitized value or the `$default_value` provided.
1834 */
1835 public function post_value( $setting, $default_value = null ) {
1836 $post_values = $this->unsanitized_post_values();
1837 if ( ! array_key_exists( $setting->id, $post_values ) ) {
1838 return $default_value;
1839 }
1840
1841 $value = $post_values[ $setting->id ];
1842 $valid = $setting->validate( $value );
1843 if ( is_wp_error( $valid ) ) {
1844 return $default_value;
1845 }
1846
1847 $value = $setting->sanitize( $value );
1848 if ( is_null( $value ) || is_wp_error( $value ) ) {
1849 return $default_value;
1850 }
1851
1852 return $value;
1853 }
1854
1855 /**
1856 * Overrides a setting's value in the current customized state.
1857 *
1858 * The name "post_value" is a carry-over from when the customized state was
1859 * exclusively sourced from `$_POST['customized']`.
1860 *
1861 * @since 4.2.0
1862 *
1863 * @param string $setting_id ID for the WP_Customize_Setting instance.
1864 * @param mixed $value Post value.
1865 */
1866 public function set_post_value( $setting_id, $value ) {
1867 $this->unsanitized_post_values(); // Populate _post_values from $_POST['customized'].
1868 $this->_post_values[ $setting_id ] = $value;
1869
1870 /**
1871 * Announces when a specific setting's unsanitized post value has been set.
1872 *
1873 * Fires when the WP_Customize_Manager::set_post_value() method is called.
1874 *
1875 * The dynamic portion of the hook name, `$setting_id`, refers to the setting ID.
1876 *
1877 * @since 4.4.0
1878 *
1879 * @param mixed $value Unsanitized setting post value.
1880 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
1881 */
1882 do_action( "customize_post_value_set_{$setting_id}", $value, $this );
1883
1884 /**
1885 * Announces when any setting's unsanitized post value has been set.
1886 *
1887 * Fires when the WP_Customize_Manager::set_post_value() method is called.
1888 *
1889 * This is useful for `WP_Customize_Setting` instances to watch
1890 * in order to update a cached previewed value.
1891 *
1892 * @since 4.4.0
1893 *
1894 * @param string $setting_id Setting ID.
1895 * @param mixed $value Unsanitized setting post value.
1896 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
1897 */
1898 do_action( 'customize_post_value_set', $setting_id, $value, $this );
1899 }
1900
1901 /**
1902 * Prints JavaScript settings.
1903 *
1904 * @since 3.4.0
1905 */
1906 public function customize_preview_init() {
1907
1908 /*
1909 * Now that Customizer previews are loaded into iframes via GET requests
1910 * and natural URLs with transaction UUIDs added, we need to ensure that
1911 * the responses are never cached by proxies. In practice, this will not
1912 * be needed if the user is logged-in anyway. But if anonymous access is
1913 * allowed then the auth cookies would not be sent and WordPress would
1914 * not send no-cache headers by default.
1915 */
1916 if ( ! headers_sent() ) {
1917 nocache_headers();
1918 header( 'X-Robots: noindex, nofollow, noarchive' );
1919 header( 'X-Robots-Tag: noindex, nofollow, noarchive' );
1920 }
1921 add_filter( 'wp_robots', 'wp_robots_no_robots' );
1922 add_filter( 'wp_headers', array( $this, 'filter_iframe_security_headers' ) );
1923
1924 /*
1925 * If preview is being served inside the customizer preview iframe, and
1926 * if the user doesn't have customize capability, then it is assumed
1927 * that the user's session has expired and they need to re-authenticate.
1928 */
1929 if ( $this->messenger_channel && ! current_user_can( 'customize' ) ) {
1930 $this->wp_die(
1931 -1,
1932 sprintf(
1933 /* translators: %s: customize_messenger_channel */
1934 __( 'Unauthorized. You may remove the %s param to preview as frontend.' ),
1935 '<code>customize_messenger_channel<code>'
1936 )
1937 );
1938 return;
1939 }
1940
1941 $this->prepare_controls();
1942
1943 add_filter( 'wp_redirect', array( $this, 'add_state_query_params' ) );
1944
1945 wp_enqueue_script( 'customize-preview' );
1946 wp_enqueue_style( 'customize-preview' );
1947 add_action( 'wp_head', array( $this, 'customize_preview_loading_style' ) );
1948 add_action( 'wp_head', array( $this, 'remove_frameless_preview_messenger_channel' ) );
1949 add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 );
1950 add_filter( 'get_edit_post_link', '__return_empty_string' );
1951
1952 /**
1953 * Fires once the Customizer preview has initialized and JavaScript
1954 * settings have been printed.
1955 *
1956 * @since 3.4.0
1957 *
1958 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
1959 */
1960 do_action( 'customize_preview_init', $this );
1961 }
1962
1963 /**
1964 * Filters the X-Frame-Options and Content-Security-Policy headers to ensure frontend can load in customizer.
1965 *
1966 * @since 4.7.0
1967 *
1968 * @param array $headers Headers.
1969 * @return array Headers.
1970 */
1971 public function filter_iframe_security_headers( $headers ) {
1972 $headers['X-Frame-Options'] = 'SAMEORIGIN';
1973 $headers['Content-Security-Policy'] = "frame-ancestors 'self'";
1974 return $headers;
1975 }
1976
1977 /**
1978 * Adds customize state query params to a given URL if preview is allowed.
1979 *
1980 * @since 4.7.0
1981 *
1982 * @see wp_redirect()
1983 * @see WP_Customize_Manager::get_allowed_url()
1984 *
1985 * @param string $url URL.
1986 * @return string URL.
1987 */
1988 public function add_state_query_params( $url ) {
1989 $parsed_original_url = wp_parse_url( $url );
1990 $is_allowed = false;
1991 foreach ( $this->get_allowed_urls() as $allowed_url ) {
1992 $parsed_allowed_url = wp_parse_url( $allowed_url );
1993 $is_allowed = (
1994 $parsed_allowed_url['scheme'] === $parsed_original_url['scheme']
1995 &&
1996 $parsed_allowed_url['host'] === $parsed_original_url['host']
1997 &&
1998 str_starts_with( $parsed_original_url['path'], $parsed_allowed_url['path'] )
1999 );
2000 if ( $is_allowed ) {
2001 break;
2002 }
2003 }
2004
2005 if ( $is_allowed ) {
2006 $query_params = array(
2007 'customize_changeset_uuid' => $this->changeset_uuid(),
2008 );
2009 if ( ! $this->is_theme_active() ) {
2010 $query_params['customize_theme'] = $this->get_stylesheet();
2011 }
2012 if ( $this->messenger_channel ) {
2013 $query_params['customize_messenger_channel'] = $this->messenger_channel;
2014 }
2015 $url = add_query_arg( $query_params, $url );
2016 }
2017
2018 return $url;
2019 }
2020
2021 /**
2022 * Prevents sending a 404 status when returning the response for the customize
2023 * preview, since it causes the jQuery Ajax to fail. Send 200 instead.
2024 *
2025 * @since 4.0.0
2026 * @deprecated 4.7.0
2027 */
2028 public function customize_preview_override_404_status() {
2029 _deprecated_function( __METHOD__, '4.7.0' );
2030 }
2031
2032 /**
2033 * Prints base element for preview frame.
2034 *
2035 * @since 3.4.0
2036 * @deprecated 4.7.0
2037 */
2038 public function customize_preview_base() {
2039 _deprecated_function( __METHOD__, '4.7.0' );
2040 }
2041
2042 /**
2043 * Prints a workaround to handle HTML5 tags in IE < 9.
2044 *
2045 * @since 3.4.0
2046 * @deprecated 4.7.0 Customizer no longer supports IE8, so all supported browsers recognize HTML5.
2047 */
2048 public function customize_preview_html5() {
2049 _deprecated_function( __FUNCTION__, '4.7.0' );
2050 }
2051
2052 /**
2053 * Prints CSS for loading indicators for the Customizer preview.
2054 *
2055 * @since 4.2.0
2056 */
2057 public function customize_preview_loading_style() {
2058 ?>
2059 <style>
2060 body.wp-customizer-unloading {
2061 opacity: 0.25;
2062 cursor: progress !important;
2063 -webkit-transition: opacity 0.5s;
2064 transition: opacity 0.5s;
2065 }
2066 body.wp-customizer-unloading * {
2067 pointer-events: none !important;
2068 }
2069 form.customize-unpreviewable,
2070 form.customize-unpreviewable input,
2071 form.customize-unpreviewable select,
2072 form.customize-unpreviewable button,
2073 a.customize-unpreviewable,
2074 area.customize-unpreviewable {
2075 cursor: not-allowed !important;
2076 }
2077 </style>
2078 <?php
2079 }
2080
2081 /**
2082 * Removes customize_messenger_channel query parameter from the preview window when it is not in an iframe.
2083 *
2084 * This ensures that the admin bar will be shown. It also ensures that link navigation will
2085 * work as expected since the parent frame is not being sent the URL to navigate to.
2086 *
2087 * @since 4.7.0
2088 */
2089 public function remove_frameless_preview_messenger_channel() {
2090 if ( ! $this->messenger_channel ) {
2091 return;
2092 }
2093 ob_start();
2094 ?>
2095 <script>
2096 ( function() {
2097 if ( parent !== window ) {
2098 return;
2099 }
2100 const url = new URL( location.href );
2101 if ( url.searchParams.has( 'customize_messenger_channel' ) ) {
2102 url.searchParams.delete( 'customize_messenger_channel' );
2103 location.replace( url );
2104 }
2105 } )();
2106 </script>
2107 <?php
2108 wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) . "\n//# sourceURL=" . rawurlencode( __METHOD__ ) );
2109 }
2110
2111 /**
2112 * Prints JavaScript settings for preview frame.
2113 *
2114 * @since 3.4.0
2115 */
2116 public function customize_preview_settings() {
2117 $post_values = $this->unsanitized_post_values( array( 'exclude_changeset' => true ) );
2118 $setting_validities = $this->validate_setting_values( $post_values );
2119 $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities );
2120
2121 // Note that the REQUEST_URI is not passed into home_url() since this breaks subdirectory installations.
2122 $self_url = empty( $_SERVER['REQUEST_URI'] ) ? home_url( '/' ) : sanitize_url( wp_unslash( $_SERVER['REQUEST_URI'] ) );
2123 $state_query_params = array(
2124 'customize_theme',
2125 'customize_changeset_uuid',
2126 'customize_messenger_channel',
2127 );
2128 $self_url = remove_query_arg( $state_query_params, $self_url );
2129
2130 $allowed_urls = $this->get_allowed_urls();
2131 $allowed_hosts = array();
2132 foreach ( $allowed_urls as $allowed_url ) {
2133 $parsed = wp_parse_url( $allowed_url );
2134 if ( empty( $parsed['host'] ) ) {
2135 continue;
2136 }
2137 $host = $parsed['host'];
2138 if ( ! empty( $parsed['port'] ) ) {
2139 $host .= ':' . $parsed['port'];
2140 }
2141 $allowed_hosts[] = $host;
2142 }
2143
2144 $switched_locale = switch_to_user_locale( get_current_user_id() );
2145 $l10n = array(
2146 'shiftClickToEdit' => __( 'Shift-click to edit this element.' ),
2147 'linkUnpreviewable' => __( 'This link is not live-previewable.' ),
2148 'formUnpreviewable' => __( 'This form is not live-previewable.' ),
2149 );
2150 if ( $switched_locale ) {
2151 restore_previous_locale();
2152 }
2153
2154 $settings = array(
2155 'changeset' => array(
2156 'uuid' => $this->changeset_uuid(),
2157 'autosaved' => $this->autosaved(),
2158 ),
2159 'timeouts' => array(
2160 'selectiveRefresh' => 250,
2161 'keepAliveSend' => 1000,
2162 ),
2163 'theme' => array(
2164 'stylesheet' => $this->get_stylesheet(),
2165 'active' => $this->is_theme_active(),
2166 'isBlockTheme' => wp_is_block_theme(),
2167 ),
2168 'url' => array(
2169 'self' => $self_url,
2170 'allowed' => array_map( 'sanitize_url', $this->get_allowed_urls() ),
2171 'allowedHosts' => array_unique( $allowed_hosts ),
2172 'isCrossDomain' => $this->is_cross_domain(),
2173 ),
2174 'channel' => $this->messenger_channel,
2175 'activePanels' => array(),
2176 'activeSections' => array(),
2177 'activeControls' => array(),
2178 'settingValidities' => $exported_setting_validities,
2179 'nonce' => current_user_can( 'customize' ) ? $this->get_nonces() : array(),
2180 'l10n' => $l10n,
2181 '_dirty' => array_keys( $post_values ),
2182 );
2183
2184 foreach ( $this->panels as $panel_id => $panel ) {
2185 if ( $panel->check_capabilities() ) {
2186 $settings['activePanels'][ $panel_id ] = $panel->active();
2187 foreach ( $panel->sections as $section_id => $section ) {
2188 if ( $section->check_capabilities() ) {
2189 $settings['activeSections'][ $section_id ] = $section->active();
2190 }
2191 }
2192 }
2193 }
2194 foreach ( $this->sections as $id => $section ) {
2195 if ( $section->check_capabilities() ) {
2196 $settings['activeSections'][ $id ] = $section->active();
2197 }
2198 }
2199 foreach ( $this->controls as $id => $control ) {
2200 if ( $control->check_capabilities() ) {
2201 $settings['activeControls'][ $id ] = $control->active();
2202 }
2203 }
2204
2205 ob_start();
2206 ?>
2207 <script>
2208 var _wpCustomizeSettings = <?php echo wp_json_encode( $settings, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ); ?>;
2209 _wpCustomizeSettings.values = {};
2210 (function( v ) {
2211 <?php
2212 /*
2213 * Serialize settings separately from the initial _wpCustomizeSettings
2214 * serialization in order to avoid a peak memory usage spike.
2215 * @todo We may not even need to export the values at all since the pane syncs them anyway.
2216 */
2217 foreach ( $this->settings as $id => $setting ) {
2218 if ( $setting->check_capabilities() ) {
2219 printf(
2220 "v[%s] = %s;\n",
2221 wp_json_encode( $id, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
2222 wp_json_encode( $setting->js_value(), JSON_HEX_TAG | JSON_UNESCAPED_SLASHES )
2223 );
2224 }
2225 }
2226 ?>
2227 })( _wpCustomizeSettings.values );
2228 </script>
2229 <?php
2230 wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) . "\n//# sourceURL=" . rawurlencode( __METHOD__ ) );
2231 }
2232
2233 /**
2234 * Prints a signature so we can ensure the Customizer was properly executed.
2235 *
2236 * @since 3.4.0
2237 * @deprecated 4.7.0
2238 */
2239 public function customize_preview_signature() {
2240 _deprecated_function( __METHOD__, '4.7.0' );
2241 }
2242
2243 /**
2244 * Removes the signature in case we experience a case where the Customizer was not properly executed.
2245 *
2246 * @since 3.4.0
2247 * @deprecated 4.7.0
2248 *
2249 * @param callable|null $callback Optional. Value passed through for {@see 'wp_die_handler'} filter.
2250 * Default null.
2251 * @return callable|null Value passed through for {@see 'wp_die_handler'} filter.
2252 */
2253 public function remove_preview_signature( $callback = null ) {
2254 _deprecated_function( __METHOD__, '4.7.0' );
2255
2256 return $callback;
2257 }
2258
2259 /**
2260 * Determines whether it is a theme preview or not.
2261 *
2262 * @since 3.4.0
2263 *
2264 * @return bool True if it's a preview, false if not.
2265 */
2266 public function is_preview() {
2267 return (bool) $this->previewing;
2268 }
2269
2270 /**
2271 * Retrieves the template name of the previewed theme.
2272 *
2273 * @since 3.4.0
2274 *
2275 * @return string Template name.
2276 */
2277 public function get_template() {
2278 return $this->theme()->get_template();
2279 }
2280
2281 /**
2282 * Retrieves the stylesheet name of the previewed theme.
2283 *
2284 * @since 3.4.0
2285 *
2286 * @return string Stylesheet name.
2287 */
2288 public function get_stylesheet() {
2289 return $this->theme()->get_stylesheet();
2290 }
2291
2292 /**
2293 * Retrieves the template root of the previewed theme.
2294 *
2295 * @since 3.4.0
2296 *
2297 * @return string Theme root.
2298 */
2299 public function get_template_root() {
2300 return get_raw_theme_root( $this->get_template(), true );
2301 }
2302
2303 /**
2304 * Retrieves the stylesheet root of the previewed theme.
2305 *
2306 * @since 3.4.0
2307 *
2308 * @return string Theme root.
2309 */
2310 public function get_stylesheet_root() {
2311 return get_raw_theme_root( $this->get_stylesheet(), true );
2312 }
2313
2314 /**
2315 * Filters the active theme and return the name of the previewed theme.
2316 *
2317 * @since 3.4.0
2318 *
2319 * @param mixed $current_theme {@internal Parameter is not used}
2320 * @return string Theme name.
2321 */
2322 public function current_theme( $current_theme ) {
2323 return $this->theme()->display( 'Name' );
2324 }
2325
2326 /**
2327 * Validates setting values.
2328 *
2329 * Validation is skipped for unregistered settings or for values that are
2330 * already null since they will be skipped anyway. Sanitization is applied
2331 * to values that pass validation, and values that become null or `WP_Error`
2332 * after sanitizing are marked invalid.
2333 *
2334 * @since 4.6.0
2335 *
2336 * @see WP_REST_Request::has_valid_params()
2337 * @see WP_Customize_Setting::validate()
2338 *
2339 * @param array $setting_values Mapping of setting IDs to values to validate and sanitize.
2340 * @param array $options {
2341 * Options.
2342 *
2343 * @type bool $validate_existence Whether a setting's existence will be checked.
2344 * @type bool $validate_capability Whether the setting capability will be checked.
2345 * }
2346 * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`.
2347 */
2348 public function validate_setting_values( $setting_values, $options = array() ) {
2349 $options = wp_parse_args(
2350 $options,
2351 array(
2352 'validate_capability' => false,
2353 'validate_existence' => false,
2354 )
2355 );
2356
2357 $validities = array();
2358 foreach ( $setting_values as $setting_id => $unsanitized_value ) {
2359 $setting = $this->get_setting( $setting_id );
2360 if ( ! $setting ) {
2361 if ( $options['validate_existence'] ) {
2362 $validities[ $setting_id ] = new WP_Error( 'unrecognized', __( 'Setting does not exist or is unrecognized.' ) );
2363 }
2364 continue;
2365 }
2366 if ( $options['validate_capability'] && ! current_user_can( $setting->capability ) ) {
2367 $validity = new WP_Error( 'unauthorized', __( 'Unauthorized to modify setting due to capability.' ) );
2368 } else {
2369 if ( is_null( $unsanitized_value ) ) {
2370 continue;
2371 }
2372 $validity = $setting->validate( $unsanitized_value );
2373 }
2374 if ( ! is_wp_error( $validity ) ) {
2375 /** This filter is documented in wp-includes/class-wp-customize-setting.php */
2376 $late_validity = apply_filters( "customize_validate_{$setting->id}", new WP_Error(), $unsanitized_value, $setting );
2377 if ( is_wp_error( $late_validity ) && $late_validity->has_errors() ) {
2378 $validity = $late_validity;
2379 }
2380 }
2381 if ( ! is_wp_error( $validity ) ) {
2382 $value = $setting->sanitize( $unsanitized_value );
2383 if ( is_null( $value ) ) {
2384 $validity = false;
2385 } elseif ( is_wp_error( $value ) ) {
2386 $validity = $value;
2387 }
2388 }
2389 if ( false === $validity ) {
2390 $validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) );
2391 }
2392 $validities[ $setting_id ] = $validity;
2393 }
2394 return $validities;
2395 }
2396
2397 /**
2398 * Prepares setting validity for exporting to the client (JS).
2399 *
2400 * Converts `WP_Error` instance into array suitable for passing into the
2401 * `wp.customize.Notification` JS model.
2402 *
2403 * @since 4.6.0
2404 *
2405 * @param true|WP_Error $validity Setting validity.
2406 * @return true|array If `$validity` was a WP_Error, the error codes will be array-mapped
2407 * to their respective `message` and `data` to pass into the
2408 * `wp.customize.Notification` JS model.
2409 */
2410 public function prepare_setting_validity_for_js( $validity ) {
2411 if ( is_wp_error( $validity ) ) {
2412 $notification = array();
2413 foreach ( $validity->errors as $error_code => $error_messages ) {
2414 $notification[ $error_code ] = array(
2415 'message' => implode( ' ', $error_messages ),
2416 'data' => $validity->get_error_data( $error_code ),
2417 );
2418 }
2419 return $notification;
2420 } else {
2421 return true;
2422 }
2423 }
2424
2425 /**
2426 * Handles customize_save WP Ajax request to save/update a changeset.
2427 *
2428 * @since 3.4.0
2429 * @since 4.7.0 The semantics of this method have changed to update a changeset, optionally to also change the status and other attributes.
2430 */
2431 public function save() {
2432 if ( ! is_user_logged_in() ) {
2433 wp_send_json_error( 'unauthenticated' );
2434 }
2435
2436 if ( ! $this->is_preview() ) {
2437 wp_send_json_error( 'not_preview' );
2438 }
2439
2440 $action = 'save-customize_' . $this->get_stylesheet();
2441 if ( ! check_ajax_referer( $action, 'nonce', false ) ) {
2442 wp_send_json_error( 'invalid_nonce' );
2443 }
2444
2445 $changeset_post_id = $this->changeset_post_id();
2446 $is_new_changeset = empty( $changeset_post_id );
2447 if ( $is_new_changeset ) {
2448 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->create_posts ) ) {
2449 wp_send_json_error( 'cannot_create_changeset_post' );
2450 }
2451 } else {
2452 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
2453 wp_send_json_error( 'cannot_edit_changeset_post' );
2454 }
2455 }
2456
2457 if ( ! empty( $_POST['customize_changeset_data'] ) ) {
2458 $input_changeset_data = json_decode( wp_unslash( $_POST['customize_changeset_data'] ), true );
2459 if ( ! is_array( $input_changeset_data ) ) {
2460 wp_send_json_error( 'invalid_customize_changeset_data' );
2461 }
2462 } else {
2463 $input_changeset_data = array();
2464 }
2465
2466 // Validate title.
2467 $changeset_title = null;
2468 if ( isset( $_POST['customize_changeset_title'] ) ) {
2469 $changeset_title = sanitize_text_field( wp_unslash( $_POST['customize_changeset_title'] ) );
2470 }
2471
2472 // Validate changeset status param.
2473 $is_publish = null;
2474 $changeset_status = null;
2475 if ( isset( $_POST['customize_changeset_status'] ) ) {
2476 $changeset_status = wp_unslash( $_POST['customize_changeset_status'] );
2477 if ( ! get_post_status_object( $changeset_status ) || ! in_array( $changeset_status, array( 'draft', 'pending', 'publish', 'future' ), true ) ) {
2478 wp_send_json_error( 'bad_customize_changeset_status', 400 );
2479 }
2480 $is_publish = ( 'publish' === $changeset_status || 'future' === $changeset_status );
2481 if ( $is_publish && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) {
2482 wp_send_json_error( 'changeset_publish_unauthorized', 403 );
2483 }
2484 }
2485
2486 /*
2487 * Validate changeset date param. Date is assumed to be in local time for
2488 * the WP if in MySQL format (YYYY-MM-DD HH:MM:SS). Otherwise, the date
2489 * is parsed with strtotime() so that ISO date format may be supplied
2490 * or a string like "+10 minutes".
2491 */
2492 $changeset_date_gmt = null;
2493 if ( isset( $_POST['customize_changeset_date'] ) ) {
2494 $changeset_date = wp_unslash( $_POST['customize_changeset_date'] );
2495 if ( preg_match( '/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/', $changeset_date ) ) {
2496 $mm = substr( $changeset_date, 5, 2 );
2497 $jj = substr( $changeset_date, 8, 2 );
2498 $aa = substr( $changeset_date, 0, 4 );
2499 $valid_date = wp_checkdate( $mm, $jj, $aa, $changeset_date );
2500 if ( ! $valid_date ) {
2501 wp_send_json_error( 'bad_customize_changeset_date', 400 );
2502 }
2503 $changeset_date_gmt = get_gmt_from_date( $changeset_date );
2504 } else {
2505 $timestamp = strtotime( $changeset_date );
2506 if ( ! $timestamp ) {
2507 wp_send_json_error( 'bad_customize_changeset_date', 400 );
2508 }
2509 $changeset_date_gmt = gmdate( 'Y-m-d H:i:s', $timestamp );
2510 }
2511 }
2512
2513 $lock_user_id = null;
2514 $autosave = ! empty( $_POST['customize_changeset_autosave'] );
2515 if ( ! $is_new_changeset ) {
2516 $lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
2517 }
2518
2519 // Force request to autosave when changeset is locked.
2520 if ( $lock_user_id && ! $autosave ) {
2521 $autosave = true;
2522 $changeset_status = null;
2523 $changeset_date_gmt = null;
2524 }
2525
2526 if ( $autosave && ! defined( 'DOING_AUTOSAVE' ) ) { // Back-compat.
2527 define( 'DOING_AUTOSAVE', true );
2528 }
2529
2530 $autosaved = false;
2531 $r = $this->save_changeset_post(
2532 array(
2533 'status' => $changeset_status,
2534 'title' => $changeset_title,
2535 'date_gmt' => $changeset_date_gmt,
2536 'data' => $input_changeset_data,
2537 'autosave' => $autosave,
2538 )
2539 );
2540 if ( $autosave && ! is_wp_error( $r ) ) {
2541 $autosaved = true;
2542 }
2543
2544 // If the changeset was locked and an autosave request wasn't itself an error, then now explicitly return with a failure.
2545 if ( $lock_user_id && ! is_wp_error( $r ) ) {
2546 $r = new WP_Error(
2547 'changeset_locked',
2548 __( 'Changeset is being edited by other user.' ),
2549 array(
2550 'lock_user' => $this->get_lock_user_data( $lock_user_id ),
2551 )
2552 );
2553 }
2554
2555 if ( is_wp_error( $r ) ) {
2556 $response = array(
2557 'message' => $r->get_error_message(),
2558 'code' => $r->get_error_code(),
2559 );
2560 if ( is_array( $r->get_error_data() ) ) {
2561 $response = array_merge( $response, $r->get_error_data() );
2562 } else {
2563 $response['data'] = $r->get_error_data();
2564 }
2565 } else {
2566 $response = $r;
2567 $changeset_post = get_post( $this->changeset_post_id() );
2568
2569 // Dismiss all other auto-draft changeset posts for this user (they serve like autosave revisions), as there should only be one.
2570 if ( $is_new_changeset ) {
2571 $this->dismiss_user_auto_draft_changesets();
2572 }
2573
2574 // Note that if the changeset status was publish, then it will get set to Trash if revisions are not supported.
2575 $response['changeset_status'] = $changeset_post->post_status;
2576 if ( $is_publish && 'trash' === $response['changeset_status'] ) {
2577 $response['changeset_status'] = 'publish';
2578 }
2579
2580 if ( 'publish' !== $response['changeset_status'] ) {
2581 $this->set_changeset_lock( $changeset_post->ID );
2582 }
2583
2584 if ( 'future' === $response['changeset_status'] ) {
2585 $response['changeset_date'] = $changeset_post->post_date;
2586 }
2587
2588 if ( 'publish' === $response['changeset_status'] || 'trash' === $response['changeset_status'] ) {
2589 $response['next_changeset_uuid'] = wp_generate_uuid4();
2590 }
2591 }
2592
2593 if ( $autosave ) {
2594 $response['autosaved'] = $autosaved;
2595 }
2596
2597 if ( isset( $response['setting_validities'] ) ) {
2598 $response['setting_validities'] = array_map( array( $this, 'prepare_setting_validity_for_js' ), $response['setting_validities'] );
2599 }
2600
2601 /**
2602 * Filters response data for a successful customize_save Ajax request.
2603 *
2604 * This filter does not apply if there was a nonce or authentication failure.
2605 *
2606 * @since 4.2.0
2607 *
2608 * @param array $response Additional information passed back to the 'saved'
2609 * event on `wp.customize`.
2610 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
2611 */
2612 $response = apply_filters( 'customize_save_response', $response, $this );
2613
2614 if ( is_wp_error( $r ) ) {
2615 wp_send_json_error( $response );
2616 } else {
2617 wp_send_json_success( $response );
2618 }
2619 }
2620
2621 /**
2622 * Saves the post for the loaded changeset.
2623 *
2624 * @since 4.7.0
2625 *
2626 * @param array $args {
2627 * Args for changeset post.
2628 *
2629 * @type array $data Optional additional changeset data. Values will be merged on top of any existing post values.
2630 * @type string $status Post status. Optional. If supplied, the save will be transactional and a post revision will be allowed.
2631 * @type string $title Post title. Optional.
2632 * @type string $date_gmt Date in GMT. Optional.
2633 * @type int $user_id ID for user who is saving the changeset. Optional, defaults to the current user ID.
2634 * @type bool $starter_content Whether the data is starter content. If false (default), then $starter_content will be cleared for any $data being saved.
2635 * @type bool $autosave Whether this is a request to create an autosave revision.
2636 * }
2637 *
2638 * @return array|WP_Error Returns array on success and WP_Error with array data on error.
2639 */
2640 public function save_changeset_post( $args = array() ) {
2641
2642 $args = array_merge(
2643 array(
2644 'status' => null,
2645 'title' => null,
2646 'data' => array(),
2647 'date_gmt' => null,
2648 'user_id' => get_current_user_id(),
2649 'starter_content' => false,
2650 'autosave' => false,
2651 ),
2652 $args
2653 );
2654
2655 $changeset_post_id = $this->changeset_post_id();
2656 $existing_changeset_data = array();
2657 if ( $changeset_post_id ) {
2658 $existing_status = get_post_status( $changeset_post_id );
2659 if ( 'publish' === $existing_status || 'trash' === $existing_status ) {
2660 return new WP_Error(
2661 'changeset_already_published',
2662 __( 'The previous set of changes has already been published. Please try saving your current set of changes again.' ),
2663 array(
2664 'next_changeset_uuid' => wp_generate_uuid4(),
2665 )
2666 );
2667 }
2668
2669 $existing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2670 if ( is_wp_error( $existing_changeset_data ) ) {
2671 return $existing_changeset_data;
2672 }
2673 }
2674
2675 // Fail if attempting to publish but publish hook is missing.
2676 if ( 'publish' === $args['status'] && false === has_action( 'transition_post_status', '_wp_customize_publish_changeset' ) ) {
2677 return new WP_Error( 'missing_publish_callback' );
2678 }
2679
2680 // Validate date.
2681 $now = gmdate( 'Y-m-d H:i:59' );
2682 if ( $args['date_gmt'] ) {
2683 $is_future_dated = ( mysql2date( 'U', $args['date_gmt'], false ) > mysql2date( 'U', $now, false ) );
2684 if ( ! $is_future_dated ) {
2685 return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) ); // Only future dates are allowed.
2686 }
2687
2688 if ( ! $this->is_theme_active() && ( 'future' === $args['status'] || $is_future_dated ) ) {
2689 return new WP_Error( 'cannot_schedule_theme_switches' ); // This should be allowed in the future, when theme is a regular setting.
2690 }
2691 $will_remain_auto_draft = ( ! $args['status'] && ( ! $changeset_post_id || 'auto-draft' === get_post_status( $changeset_post_id ) ) );
2692 if ( $will_remain_auto_draft ) {
2693 return new WP_Error( 'cannot_supply_date_for_auto_draft_changeset' );
2694 }
2695 } elseif ( $changeset_post_id && 'future' === $args['status'] ) {
2696
2697 // Fail if the new status is future but the existing post's date is not in the future.
2698 $changeset_post = get_post( $changeset_post_id );
2699 if ( mysql2date( 'U', $changeset_post->post_date_gmt, false ) <= mysql2date( 'U', $now, false ) ) {
2700 return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) );
2701 }
2702 }
2703
2704 if ( ! empty( $is_future_dated ) && 'publish' === $args['status'] ) {
2705 $args['status'] = 'future';
2706 }
2707
2708 // Validate autosave param. See _wp_post_revision_fields() for why these fields are disallowed.
2709 if ( $args['autosave'] ) {
2710 if ( $args['date_gmt'] ) {
2711 return new WP_Error( 'illegal_autosave_with_date_gmt' );
2712 } elseif ( $args['status'] ) {
2713 return new WP_Error( 'illegal_autosave_with_status' );
2714 } elseif ( $args['user_id'] && get_current_user_id() !== $args['user_id'] ) {
2715 return new WP_Error( 'illegal_autosave_with_non_current_user' );
2716 }
2717 }
2718
2719 // The request was made via wp.customize.previewer.save().
2720 $update_transactionally = (bool) $args['status'];
2721 $allow_revision = (bool) $args['status'];
2722
2723 // Amend post values with any supplied data.
2724 foreach ( $args['data'] as $setting_id => $setting_params ) {
2725 if ( is_array( $setting_params ) && array_key_exists( 'value', $setting_params ) ) {
2726 $this->set_post_value( $setting_id, $setting_params['value'] ); // Add to post values so that they can be validated and sanitized.
2727 }
2728 }
2729
2730 // Note that in addition to post data, this will include any stashed theme mods.
2731 $post_values = $this->unsanitized_post_values(
2732 array(
2733 'exclude_changeset' => true,
2734 'exclude_post_data' => false,
2735 )
2736 );
2737 $this->add_dynamic_settings( array_keys( $post_values ) ); // Ensure settings get created even if they lack an input value.
2738
2739 /*
2740 * Get list of IDs for settings that have values different from what is currently
2741 * saved in the changeset. By skipping any values that are already the same, the
2742 * subset of changed settings can be passed into validate_setting_values to prevent
2743 * an underprivileged modifying a single setting for which they have the capability
2744 * from being blocked from saving. This also prevents a user from touching of the
2745 * previous saved settings and overriding the associated user_id if they made no change.
2746 */
2747 $changed_setting_ids = array();
2748 foreach ( $post_values as $setting_id => $setting_value ) {
2749 $setting = $this->get_setting( $setting_id );
2750
2751 if ( $setting && 'theme_mod' === $setting->type ) {
2752 $prefixed_setting_id = $this->get_stylesheet() . '::' . $setting->id;
2753 } else {
2754 $prefixed_setting_id = $setting_id;
2755 }
2756
2757 $is_value_changed = (
2758 ! isset( $existing_changeset_data[ $prefixed_setting_id ] )
2759 ||
2760 ! array_key_exists( 'value', $existing_changeset_data[ $prefixed_setting_id ] )
2761 ||
2762 $existing_changeset_data[ $prefixed_setting_id ]['value'] !== $setting_value
2763 );
2764 if ( $is_value_changed ) {
2765 $changed_setting_ids[] = $setting_id;
2766 }
2767 }
2768
2769 /**
2770 * Fires before save validation happens.
2771 *
2772 * Plugins can add just-in-time {@see 'customize_validate_{$this->ID}'} filters
2773 * at this point to catch any settings registered after `customize_register`.
2774 * The dynamic portion of the hook name, `$this->ID` refers to the setting ID.
2775 *
2776 * @since 4.6.0
2777 *
2778 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
2779 */
2780 do_action( 'customize_save_validation_before', $this );
2781
2782 // Validate settings.
2783 $validated_values = array_merge(
2784 array_fill_keys( array_keys( $args['data'] ), null ), // Make sure existence/capability checks are done on value-less setting updates.
2785 $post_values
2786 );
2787 $setting_validities = $this->validate_setting_values(
2788 $validated_values,
2789 array(
2790 'validate_capability' => true,
2791 'validate_existence' => true,
2792 )
2793 );
2794 $invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) );
2795
2796 /*
2797 * Short-circuit if there are invalid settings the update is transactional.
2798 * A changeset update is transactional when a status is supplied in the request.
2799 */
2800 if ( $update_transactionally && $invalid_setting_count > 0 ) {
2801 $response = array(
2802 'setting_validities' => $setting_validities,
2803 /* translators: %s: Number of invalid settings. */
2804 'message' => sprintf( _n( 'Unable to save due to %s invalid setting.', 'Unable to save due to %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
2805 );
2806 return new WP_Error( 'transaction_fail', '', $response );
2807 }
2808
2809 // Obtain/merge data for changeset.
2810 $original_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
2811 $data = $original_changeset_data;
2812 if ( is_wp_error( $data ) ) {
2813 $data = array();
2814 }
2815
2816 // Ensure that all post values are included in the changeset data.
2817 foreach ( $post_values as $setting_id => $post_value ) {
2818 if ( ! isset( $args['data'][ $setting_id ] ) ) {
2819 $args['data'][ $setting_id ] = array();
2820 }
2821 if ( ! isset( $args['data'][ $setting_id ]['value'] ) ) {
2822 $args['data'][ $setting_id ]['value'] = $post_value;
2823 }
2824 }
2825
2826 foreach ( $args['data'] as $setting_id => $setting_params ) {
2827 $setting = $this->get_setting( $setting_id );
2828 if ( ! $setting || ! $setting->check_capabilities() ) {
2829 continue;
2830 }
2831
2832 // Skip updating changeset for invalid setting values.
2833 if ( isset( $setting_validities[ $setting_id ] ) && is_wp_error( $setting_validities[ $setting_id ] ) ) {
2834 continue;
2835 }
2836
2837 $changeset_setting_id = $setting_id;
2838 if ( 'theme_mod' === $setting->type ) {
2839 $changeset_setting_id = sprintf( '%s::%s', $this->get_stylesheet(), $setting_id );
2840 }
2841
2842 if ( null === $setting_params ) {
2843 // Remove setting from changeset entirely.
2844 unset( $data[ $changeset_setting_id ] );
2845 } else {
2846
2847 if ( ! isset( $data[ $changeset_setting_id ] ) ) {
2848 $data[ $changeset_setting_id ] = array();
2849 }
2850
2851 // Merge any additional setting params that have been supplied with the existing params.
2852 $merged_setting_params = array_merge( $data[ $changeset_setting_id ], $setting_params );
2853
2854 // Skip updating setting params if unchanged (ensuring the user_id is not overwritten).
2855 if ( $data[ $changeset_setting_id ] === $merged_setting_params ) {
2856 continue;
2857 }
2858
2859 $data[ $changeset_setting_id ] = array_merge(
2860 $merged_setting_params,
2861 array(
2862 'type' => $setting->type,
2863 'user_id' => $args['user_id'],
2864 'date_modified_gmt' => current_time( 'mysql', true ),
2865 )
2866 );
2867
2868 // Clear starter_content flag in data if changeset is not explicitly being updated for starter content.
2869 if ( empty( $args['starter_content'] ) ) {
2870 unset( $data[ $changeset_setting_id ]['starter_content'] );
2871 }
2872 }
2873 }
2874
2875 $filter_context = array(
2876 'uuid' => $this->changeset_uuid(),
2877 'title' => $args['title'],
2878 'status' => $args['status'],
2879 'date_gmt' => $args['date_gmt'],
2880 'post_id' => $changeset_post_id,
2881 'previous_data' => is_wp_error( $original_changeset_data ) ? array() : $original_changeset_data,
2882 'manager' => $this,
2883 );
2884
2885 /**
2886 * Filters the settings' data that will be persisted into the changeset.
2887 *
2888 * Plugins may amend additional data (such as additional meta for settings) into the changeset with this filter.
2889 *
2890 * @since 4.7.0
2891 *
2892 * @param array $data Updated changeset data, mapping setting IDs to arrays containing a $value item and optionally other metadata.
2893 * @param array $context {
2894 * Filter context.
2895 *
2896 * @type string $uuid Changeset UUID.
2897 * @type string $title Requested title for the changeset post.
2898 * @type string $status Requested status for the changeset post.
2899 * @type string $date_gmt Requested date for the changeset post in MySQL format and GMT timezone.
2900 * @type int|false $post_id Post ID for the changeset, or false if it doesn't exist yet.
2901 * @type array $previous_data Previous data contained in the changeset.
2902 * @type WP_Customize_Manager $manager Manager instance.
2903 * }
2904 */
2905 $data = apply_filters( 'customize_changeset_save_data', $data, $filter_context );
2906
2907 // Switch theme if publishing changes now.
2908 if ( 'publish' === $args['status'] && ! $this->is_theme_active() ) {
2909 // Temporarily stop previewing the theme to allow switch_themes() to operate properly.
2910 $this->stop_previewing_theme();
2911 switch_theme( $this->get_stylesheet() );
2912 update_option( 'theme_switched_via_customizer', true );
2913 $this->start_previewing_theme();
2914 }
2915
2916 // Gather the data for wp_insert_post()/wp_update_post().
2917 $post_array = array(
2918 // JSON_UNESCAPED_SLASHES is only to improve readability as slashes needn't be escaped in storage.
2919 'post_content' => wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT ),
2920 );
2921 if ( $args['title'] ) {
2922 $post_array['post_title'] = $args['title'];
2923 }
2924 if ( $changeset_post_id ) {
2925 $post_array['ID'] = $changeset_post_id;
2926 } else {
2927 $post_array['post_type'] = 'customize_changeset';
2928 $post_array['post_name'] = $this->changeset_uuid();
2929 $post_array['post_status'] = 'auto-draft';
2930 }
2931 if ( $args['status'] ) {
2932 $post_array['post_status'] = $args['status'];
2933 }
2934
2935 // Reset post date to now if we are publishing, otherwise pass post_date_gmt and translate for post_date.
2936 if ( 'publish' === $args['status'] ) {
2937 $post_array['post_date_gmt'] = '0000-00-00 00:00:00';
2938 $post_array['post_date'] = '0000-00-00 00:00:00';
2939 } elseif ( $args['date_gmt'] ) {
2940 $post_array['post_date_gmt'] = $args['date_gmt'];
2941 $post_array['post_date'] = get_date_from_gmt( $args['date_gmt'] );
2942 } elseif ( $changeset_post_id && 'auto-draft' === get_post_status( $changeset_post_id ) ) {
2943 /*
2944 * Keep bumping the date for the auto-draft whenever it is modified;
2945 * this extends its life, preserving it from garbage-collection via
2946 * wp_delete_auto_drafts().
2947 */
2948 $post_array['post_date'] = current_time( 'mysql' );
2949 $post_array['post_date_gmt'] = '';
2950 }
2951
2952 $this->store_changeset_revision = $allow_revision;
2953 add_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ), 5, 3 );
2954
2955 /*
2956 * Update the changeset post. The publish_customize_changeset action will cause the settings in the
2957 * changeset to be saved via WP_Customize_Setting::save(). Updating a post with publish status will
2958 * trigger WP_Customize_Manager::publish_changeset_values().
2959 */
2960 add_filter( 'wp_insert_post_data', array( $this, 'preserve_insert_changeset_post_content' ), 5, 3 );
2961 if ( $changeset_post_id ) {
2962 if ( $args['autosave'] && 'auto-draft' !== get_post_status( $changeset_post_id ) ) {
2963 // See _wp_translate_postdata() for why this is required as it will use the edit_post meta capability.
2964 add_filter( 'map_meta_cap', array( $this, 'grant_edit_post_capability_for_changeset' ), 10, 4 );
2965
2966 $post_array['post_ID'] = $post_array['ID'];
2967 $post_array['post_type'] = 'customize_changeset';
2968
2969 $r = wp_create_post_autosave( wp_slash( $post_array ) );
2970
2971 remove_filter( 'map_meta_cap', array( $this, 'grant_edit_post_capability_for_changeset' ), 10 );
2972 } else {
2973 $post_array['edit_date'] = true; // Prevent date clearing.
2974
2975 $r = wp_update_post( wp_slash( $post_array ), true );
2976
2977 // Delete autosave revision for user when the changeset is updated.
2978 if ( ! empty( $args['user_id'] ) ) {
2979 $autosave_draft = wp_get_post_autosave( $changeset_post_id, $args['user_id'] );
2980 if ( $autosave_draft ) {
2981 wp_delete_post( $autosave_draft->ID, true );
2982 }
2983 }
2984 }
2985 } else {
2986 $r = wp_insert_post( wp_slash( $post_array ), true );
2987 if ( ! is_wp_error( $r ) ) {
2988 $this->_changeset_post_id = $r; // Update cached post ID for the loaded changeset.
2989 }
2990 }
2991 remove_filter( 'wp_insert_post_data', array( $this, 'preserve_insert_changeset_post_content' ), 5 );
2992
2993 $this->_changeset_data = null; // Reset so WP_Customize_Manager::changeset_data() will re-populate with updated contents.
2994
2995 remove_filter( 'wp_save_post_revision_post_has_changed', array( $this, '_filter_revision_post_has_changed' ) );
2996
2997 $response = array(
2998 'setting_validities' => $setting_validities,
2999 );
3000
3001 if ( is_wp_error( $r ) ) {
3002 $response['changeset_post_save_failure'] = $r->get_error_code();
3003 return new WP_Error( 'changeset_post_save_failure', '', $response );
3004 }
3005
3006 return $response;
3007 }
3008
3009 /**
3010 * Preserves the initial JSON post_content passed to save into the post.
3011 *
3012 * This is needed to prevent KSES and other {@see 'content_save_pre'} filters
3013 * from corrupting JSON data.
3014 *
3015 * Note that WP_Customize_Manager::validate_setting_values() have already
3016 * run on the setting values being serialized as JSON into the post content
3017 * so it is pre-sanitized.
3018 *
3019 * Also, the sanitization logic is re-run through the respective
3020 * WP_Customize_Setting::sanitize() method when being read out of the
3021 * changeset, via WP_Customize_Manager::post_value(), and this sanitized
3022 * value will also be sent into WP_Customize_Setting::update() for
3023 * persisting to the DB.
3024 *
3025 * Multiple users can collaborate on a single changeset, where one user may
3026 * have the unfiltered_html capability but another may not. A user with
3027 * unfiltered_html may add a script tag to some field which needs to be kept
3028 * intact even when another user updates the changeset to modify another field
3029 * when they do not have unfiltered_html.
3030 *
3031 * @since 5.4.1
3032 *
3033 * @param array $data An array of slashed and processed post data.
3034 * @param array $postarr An array of sanitized (and slashed) but otherwise unmodified post data.
3035 * @param array $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed post data as originally passed to wp_insert_post().
3036 * @return array Filtered post data.
3037 */
3038 public function preserve_insert_changeset_post_content( $data, $postarr, $unsanitized_postarr ) {
3039 if (
3040 isset( $data['post_type'] ) &&
3041 isset( $unsanitized_postarr['post_content'] ) &&
3042 'customize_changeset' === $data['post_type'] ||
3043 (
3044 'revision' === $data['post_type'] &&
3045 ! empty( $data['post_parent'] ) &&
3046 'customize_changeset' === get_post_type( $data['post_parent'] )
3047 )
3048 ) {
3049 $data['post_content'] = $unsanitized_postarr['post_content'];
3050 }
3051 return $data;
3052 }
3053
3054 /**
3055 * Trashes or deletes a changeset post.
3056 *
3057 * The following re-formulates the logic from `wp_trash_post()` as done in
3058 * `wp_publish_post()`. The reason for bypassing `wp_trash_post()` is that it
3059 * will mutate the the `post_content` and the `post_name` when they should be
3060 * untouched.
3061 *
3062 * @since 4.9.0
3063 *
3064 * @see wp_trash_post()
3065 * @global wpdb $wpdb WordPress database abstraction object.
3066 *
3067 * @param int|WP_Post $post The changeset post.
3068 * @return mixed A WP_Post object for the trashed post or an empty value on failure.
3069 */
3070 public function trash_changeset_post( $post ) {
3071 global $wpdb;
3072
3073 $post = get_post( $post );
3074
3075 if ( ! ( $post instanceof WP_Post ) ) {
3076 return $post;
3077 }
3078 $post_id = $post->ID;
3079
3080 if ( ! EMPTY_TRASH_DAYS ) {
3081 return wp_delete_post( $post_id, true );
3082 }
3083
3084 if ( 'trash' === get_post_status( $post ) ) {
3085 return false;
3086 }
3087
3088 $previous_status = $post->post_status;
3089
3090 /** This filter is documented in wp-includes/post.php */
3091 $check = apply_filters( 'pre_trash_post', null, $post, $previous_status );
3092 if ( null !== $check ) {
3093 return $check;
3094 }
3095
3096 /** This action is documented in wp-includes/post.php */
3097 do_action( 'wp_trash_post', $post_id, $previous_status );
3098
3099 add_post_meta( $post_id, '_wp_trash_meta_status', $previous_status );
3100 add_post_meta( $post_id, '_wp_trash_meta_time', time() );
3101
3102 $new_status = 'trash';
3103 $wpdb->update( $wpdb->posts, array( 'post_status' => $new_status ), array( 'ID' => $post->ID ) );
3104 clean_post_cache( $post->ID );
3105
3106 $post->post_status = $new_status;
3107 wp_transition_post_status( $new_status, $previous_status, $post );
3108
3109 /** This action is documented in wp-includes/post.php */
3110 do_action( "edit_post_{$post->post_type}", $post->ID, $post );
3111
3112 /** This action is documented in wp-includes/post.php */
3113 do_action( 'edit_post', $post->ID, $post );
3114
3115 /** This action is documented in wp-includes/post.php */
3116 do_action( "save_post_{$post->post_type}", $post->ID, $post, true );
3117
3118 /** This action is documented in wp-includes/post.php */
3119 do_action( 'save_post', $post->ID, $post, true );
3120
3121 /** This action is documented in wp-includes/post.php */
3122 do_action( 'wp_insert_post', $post->ID, $post, true );
3123
3124 wp_after_insert_post( get_post( $post_id ), true, $post );
3125
3126 wp_trash_post_comments( $post_id );
3127
3128 /** This action is documented in wp-includes/post.php */
3129 do_action( 'trashed_post', $post_id, $previous_status );
3130
3131 return $post;
3132 }
3133
3134 /**
3135 * Handles request to trash a changeset.
3136 *
3137 * @since 4.9.0
3138 */
3139 public function handle_changeset_trash_request() {
3140 if ( ! is_user_logged_in() ) {
3141 wp_send_json_error( 'unauthenticated' );
3142 }
3143
3144 if ( ! $this->is_preview() ) {
3145 wp_send_json_error( 'not_preview' );
3146 }
3147
3148 if ( ! check_ajax_referer( 'trash_customize_changeset', 'nonce', false ) ) {
3149 wp_send_json_error(
3150 array(
3151 'code' => 'invalid_nonce',
3152 'message' => __( 'There was an authentication problem. Please reload and try again.' ),
3153 )
3154 );
3155 }
3156
3157 $changeset_post_id = $this->changeset_post_id();
3158
3159 if ( ! $changeset_post_id ) {
3160 wp_send_json_error(
3161 array(
3162 'message' => __( 'No changes saved yet, so there is nothing to trash.' ),
3163 'code' => 'non_existent_changeset',
3164 )
3165 );
3166 return;
3167 }
3168
3169 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
3170 wp_send_json_error(
3171 array(
3172 'code' => 'changeset_trash_unauthorized',
3173 'message' => __( 'Unable to trash changes.' ),
3174 )
3175 );
3176 }
3177
3178 $lock_user = (int) wp_check_post_lock( $changeset_post_id );
3179
3180 if ( $lock_user && get_current_user_id() !== $lock_user ) {
3181 wp_send_json_error(
3182 array(
3183 'code' => 'changeset_locked',
3184 'message' => __( 'Changeset is being edited by other user.' ),
3185 'lockUser' => $this->get_lock_user_data( $lock_user ),
3186 )
3187 );
3188 }
3189
3190 if ( 'trash' === get_post_status( $changeset_post_id ) ) {
3191 wp_send_json_error(
3192 array(
3193 'message' => __( 'Changes have already been trashed.' ),
3194 'code' => 'changeset_already_trashed',
3195 )
3196 );
3197 return;
3198 }
3199
3200 $r = $this->trash_changeset_post( $changeset_post_id );
3201 if ( ! ( $r instanceof WP_Post ) ) {
3202 wp_send_json_error(
3203 array(
3204 'code' => 'changeset_trash_failure',
3205 'message' => __( 'Unable to trash changes.' ),
3206 )
3207 );
3208 }
3209
3210 wp_send_json_success(
3211 array(
3212 'message' => __( 'Changes trashed successfully.' ),
3213 )
3214 );
3215 }
3216
3217 /**
3218 * Re-maps 'edit_post' meta cap for a customize_changeset post to be the same as 'customize' maps.
3219 *
3220 * There is essentially a "meta meta" cap in play here, where 'edit_post' meta cap maps to
3221 * the 'customize' meta cap which then maps to 'edit_theme_options'. This is currently
3222 * required in core for `wp_create_post_autosave()` because it will call
3223 * `_wp_translate_postdata()` which in turn will check if a user can 'edit_post', but the
3224 * the caps for the customize_changeset post type are all mapping to the meta capability.
3225 * This should be able to be removed once #40922 is addressed in core.
3226 *
3227 * @since 4.9.0
3228 *
3229 * @link https://core.trac.wordpress.org/ticket/40922
3230 * @see WP_Customize_Manager::save_changeset_post()
3231 * @see _wp_translate_postdata()
3232 *
3233 * @param string[] $caps Array of the user's capabilities.
3234 * @param string $cap Capability name.
3235 * @param int $user_id The user ID.
3236 * @param array $args Adds the context to the cap. Typically the object ID.
3237 * @return array Capabilities.
3238 */
3239 public function grant_edit_post_capability_for_changeset( $caps, $cap, $user_id, $args ) {
3240 if ( 'edit_post' === $cap && ! empty( $args[0] ) && 'customize_changeset' === get_post_type( $args[0] ) ) {
3241 $post_type_obj = get_post_type_object( 'customize_changeset' );
3242 $caps = map_meta_cap( $post_type_obj->cap->$cap, $user_id );
3243 }
3244 return $caps;
3245 }
3246
3247 /**
3248 * Marks the changeset post as being currently edited by the current user.
3249 *
3250 * @since 4.9.0
3251 *
3252 * @param int $changeset_post_id Changeset post ID.
3253 * @param bool $take_over Whether to take over the changeset. Default false.
3254 */
3255 public function set_changeset_lock( $changeset_post_id, $take_over = false ) {
3256 if ( $changeset_post_id ) {
3257 $can_override = ! (bool) get_post_meta( $changeset_post_id, '_edit_lock', true );
3258
3259 if ( $take_over ) {
3260 $can_override = true;
3261 }
3262
3263 if ( $can_override ) {
3264 $lock = sprintf( '%s:%s', time(), get_current_user_id() );
3265 update_post_meta( $changeset_post_id, '_edit_lock', $lock );
3266 } else {
3267 $this->refresh_changeset_lock( $changeset_post_id );
3268 }
3269 }
3270 }
3271
3272 /**
3273 * Refreshes changeset lock with the current time if current user edited the changeset before.
3274 *
3275 * @since 4.9.0
3276 *
3277 * @param int $changeset_post_id Changeset post ID.
3278 */
3279 public function refresh_changeset_lock( $changeset_post_id ) {
3280 if ( ! $changeset_post_id ) {
3281 return;
3282 }
3283
3284 $lock = get_post_meta( $changeset_post_id, '_edit_lock', true );
3285 $lock = explode( ':', $lock );
3286
3287 if ( $lock && ! empty( $lock[1] ) ) {
3288 $user_id = (int) $lock[1];
3289 $current_user_id = get_current_user_id();
3290 if ( $user_id === $current_user_id ) {
3291 $lock = sprintf( '%s:%s', time(), $user_id );
3292 update_post_meta( $changeset_post_id, '_edit_lock', $lock );
3293 }
3294 }
3295 }
3296
3297 /**
3298 * Filters heartbeat settings for the Customizer.
3299 *
3300 * @since 4.9.0
3301 *
3302 * @global string $pagenow The filename of the current screen.
3303 *
3304 * @param array $settings Current settings to filter.
3305 * @return array Heartbeat settings.
3306 */
3307 public function add_customize_screen_to_heartbeat_settings( $settings ) {
3308 global $pagenow;
3309
3310 if ( 'customize.php' === $pagenow ) {
3311 $settings['screenId'] = 'customize';
3312 }
3313
3314 return $settings;
3315 }
3316
3317 /**
3318 * Gets lock user data.
3319 *
3320 * @since 4.9.0
3321 *
3322 * @param int $user_id User ID.
3323 * @return array|null User data formatted for client.
3324 */
3325 protected function get_lock_user_data( $user_id ) {
3326 if ( ! $user_id ) {
3327 return null;
3328 }
3329
3330 $lock_user = get_userdata( $user_id );
3331
3332 if ( ! $lock_user ) {
3333 return null;
3334 }
3335
3336 $user_details = array(
3337 'id' => $lock_user->ID,
3338 'name' => $lock_user->display_name,
3339 );
3340
3341 if ( get_option( 'show_avatars' ) ) {
3342 $user_details['avatar'] = get_avatar_url( $lock_user->ID, array( 'size' => 128 ) );
3343 }
3344
3345 return $user_details;
3346 }
3347
3348 /**
3349 * Checks locked changeset with heartbeat API.
3350 *
3351 * @since 4.9.0
3352 *
3353 * @param array $response The Heartbeat response.
3354 * @param array $data The $_POST data sent.
3355 * @param string $screen_id The screen id.
3356 * @return array The Heartbeat response.
3357 */
3358 public function check_changeset_lock_with_heartbeat( $response, $data, $screen_id ) {
3359 if ( isset( $data['changeset_uuid'] ) ) {
3360 $changeset_post_id = $this->find_changeset_post_id( $data['changeset_uuid'] );
3361 } else {
3362 $changeset_post_id = $this->changeset_post_id();
3363 }
3364
3365 if (
3366 array_key_exists( 'check_changeset_lock', $data )
3367 && 'customize' === $screen_id
3368 && $changeset_post_id
3369 && current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id )
3370 ) {
3371 $lock_user_id = wp_check_post_lock( $changeset_post_id );
3372
3373 if ( $lock_user_id ) {
3374 $response['customize_changeset_lock_user'] = $this->get_lock_user_data( $lock_user_id );
3375 } else {
3376
3377 // Refreshing time will ensure that the user is sitting on customizer and has not closed the customizer tab.
3378 $this->refresh_changeset_lock( $changeset_post_id );
3379 }
3380 }
3381
3382 return $response;
3383 }
3384
3385 /**
3386 * Removes changeset lock when take over request is sent via Ajax.
3387 *
3388 * @since 4.9.0
3389 */
3390 public function handle_override_changeset_lock_request() {
3391 if ( ! $this->is_preview() ) {
3392 wp_send_json_error( 'not_preview', 400 );
3393 }
3394
3395 if ( ! check_ajax_referer( 'customize_override_changeset_lock', 'nonce', false ) ) {
3396 wp_send_json_error(
3397 array(
3398 'code' => 'invalid_nonce',
3399 'message' => __( 'Security check failed.' ),
3400 )
3401 );
3402 }
3403
3404 $changeset_post_id = $this->changeset_post_id();
3405
3406 if ( empty( $changeset_post_id ) ) {
3407 wp_send_json_error(
3408 array(
3409 'code' => 'no_changeset_found_to_take_over',
3410 'message' => __( 'No changeset found to take over' ),
3411 )
3412 );
3413 }
3414
3415 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) ) {
3416 wp_send_json_error(
3417 array(
3418 'code' => 'cannot_remove_changeset_lock',
3419 'message' => __( 'Sorry, you are not allowed to take over.' ),
3420 )
3421 );
3422 }
3423
3424 $this->set_changeset_lock( $changeset_post_id, true );
3425
3426 wp_send_json_success( 'changeset_taken_over' );
3427 }
3428
3429 /**
3430 * Determines whether a changeset revision should be made.
3431 *
3432 * @since 4.7.0
3433 * @var bool
3434 */
3435 protected $store_changeset_revision;
3436
3437 /**
3438 * Filters whether a changeset has changed to create a new revision.
3439 *
3440 * Note that this will not be called while a changeset post remains in auto-draft status.
3441 *
3442 * @since 4.7.0
3443 *
3444 * @param bool $post_has_changed Whether the post has changed.
3445 * @param WP_Post $latest_revision The latest revision post object.
3446 * @param WP_Post $post The post object.
3447 * @return bool Whether a revision should be made.
3448 */
3449 public function _filter_revision_post_has_changed( $post_has_changed, $latest_revision, $post ) {
3450 unset( $latest_revision );
3451 if ( 'customize_changeset' === $post->post_type ) {
3452 $post_has_changed = $this->store_changeset_revision;
3453 }
3454 return $post_has_changed;
3455 }
3456
3457 /**
3458 * Publishes the values of a changeset.
3459 *
3460 * This will publish the values contained in a changeset, even changesets that do not
3461 * correspond to current manager instance. This is called by
3462 * `_wp_customize_publish_changeset()` when a customize_changeset post is
3463 * transitioned to the `publish` status. As such, this method should not be
3464 * called directly and instead `wp_publish_post()` should be used.
3465 *
3466 * Please note that if the settings in the changeset are for a non-activated
3467 * theme, the theme must first be switched to (via `switch_theme()`) before
3468 * invoking this method.
3469 *
3470 * @since 4.7.0
3471 *
3472 * @see _wp_customize_publish_changeset()
3473 * @global wpdb $wpdb WordPress database abstraction object.
3474 *
3475 * @param int $changeset_post_id ID for customize_changeset post. Defaults to the changeset for the current manager instance.
3476 * @return true|WP_Error True or error info.
3477 */
3478 public function _publish_changeset_values( $changeset_post_id ) {
3479 global $wpdb;
3480
3481 $publishing_changeset_data = $this->get_changeset_post_data( $changeset_post_id );
3482 if ( is_wp_error( $publishing_changeset_data ) ) {
3483 return $publishing_changeset_data;
3484 }
3485
3486 $changeset_post = get_post( $changeset_post_id );
3487
3488 /*
3489 * Temporarily override the changeset context so that it will be read
3490 * in calls to unsanitized_post_values() and so that it will be available
3491 * on the $wp_customize object passed to hooks during the save logic.
3492 */
3493 $previous_changeset_post_id = $this->_changeset_post_id;
3494 $this->_changeset_post_id = $changeset_post_id;
3495 $previous_changeset_uuid = $this->_changeset_uuid;
3496 $this->_changeset_uuid = $changeset_post->post_name;
3497 $previous_changeset_data = $this->_changeset_data;
3498 $this->_changeset_data = $publishing_changeset_data;
3499
3500 // Parse changeset data to identify theme mod settings and user IDs associated with settings to be saved.
3501 $setting_user_ids = array();
3502 $theme_mod_settings = array();
3503 $namespace_pattern = '/^(?P<stylesheet>.+?)::(?P<setting_id>.+)$/';
3504 $matches = array();
3505 foreach ( $this->_changeset_data as $raw_setting_id => $setting_params ) {
3506 $actual_setting_id = null;
3507 $is_theme_mod_setting = (
3508 isset( $setting_params['value'] )
3509 &&
3510 isset( $setting_params['type'] )
3511 &&
3512 'theme_mod' === $setting_params['type']
3513 &&
3514 preg_match( $namespace_pattern, $raw_setting_id, $matches )
3515 );
3516 if ( $is_theme_mod_setting ) {
3517 if ( ! isset( $theme_mod_settings[ $matches['stylesheet'] ] ) ) {
3518 $theme_mod_settings[ $matches['stylesheet'] ] = array();
3519 }
3520 $theme_mod_settings[ $matches['stylesheet'] ][ $matches['setting_id'] ] = $setting_params;
3521
3522 if ( $this->get_stylesheet() === $matches['stylesheet'] ) {
3523 $actual_setting_id = $matches['setting_id'];
3524 }
3525 } else {
3526 $actual_setting_id = $raw_setting_id;
3527 }
3528
3529 // Keep track of the user IDs for settings actually for this theme.
3530 if ( $actual_setting_id && isset( $setting_params['user_id'] ) ) {
3531 $setting_user_ids[ $actual_setting_id ] = $setting_params['user_id'];
3532 }
3533 }
3534
3535 $changeset_setting_values = $this->unsanitized_post_values(
3536 array(
3537 'exclude_post_data' => true,
3538 'exclude_changeset' => false,
3539 )
3540 );
3541 $changeset_setting_ids = array_keys( $changeset_setting_values );
3542 $this->add_dynamic_settings( $changeset_setting_ids );
3543
3544 /**
3545 * Fires once the theme has switched in the Customizer, but before settings
3546 * have been saved.
3547 *
3548 * @since 3.4.0
3549 *
3550 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
3551 */
3552 do_action( 'customize_save', $this );
3553
3554 /*
3555 * Ensure that all settings will allow themselves to be saved. Note that
3556 * this is safe because the setting would have checked the capability
3557 * when the setting value was written into the changeset. So this is why
3558 * an additional capability check is not required here.
3559 */
3560 $original_setting_capabilities = array();
3561 foreach ( $changeset_setting_ids as $setting_id ) {
3562 $setting = $this->get_setting( $setting_id );
3563 if ( $setting && ! isset( $setting_user_ids[ $setting_id ] ) ) {
3564 $original_setting_capabilities[ $setting->id ] = $setting->capability;
3565 $setting->capability = 'exist';
3566 }
3567 }
3568
3569 $original_user_id = get_current_user_id();
3570 foreach ( $changeset_setting_ids as $setting_id ) {
3571 $setting = $this->get_setting( $setting_id );
3572 if ( $setting ) {
3573 /*
3574 * Set the current user to match the user who saved the value into
3575 * the changeset so that any filters that apply during the save
3576 * process will respect the original user's capabilities. This
3577 * will ensure, for example, that KSES won't strip unsafe HTML
3578 * when a scheduled changeset publishes via WP Cron.
3579 */
3580 if ( isset( $setting_user_ids[ $setting_id ] ) ) {
3581 wp_set_current_user( $setting_user_ids[ $setting_id ] );
3582 } else {
3583 wp_set_current_user( $original_user_id );
3584 }
3585
3586 $setting->save();
3587 }
3588 }
3589 wp_set_current_user( $original_user_id );
3590
3591 // Update the stashed theme mod settings, removing the active theme's stashed settings, if activated.
3592 if ( did_action( 'switch_theme' ) ) {
3593 $other_theme_mod_settings = $theme_mod_settings;
3594 unset( $other_theme_mod_settings[ $this->get_stylesheet() ] );
3595 $this->update_stashed_theme_mod_settings( $other_theme_mod_settings );
3596 }
3597
3598 /**
3599 * Fires after Customize settings have been saved.
3600 *
3601 * @since 3.6.0
3602 *
3603 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
3604 */
3605 do_action( 'customize_save_after', $this );
3606
3607 // Restore original capabilities.
3608 foreach ( $original_setting_capabilities as $setting_id => $capability ) {
3609 $setting = $this->get_setting( $setting_id );
3610 if ( $setting ) {
3611 $setting->capability = $capability;
3612 }
3613 }
3614
3615 // Restore original changeset data.
3616 $this->_changeset_data = $previous_changeset_data;
3617 $this->_changeset_post_id = $previous_changeset_post_id;
3618 $this->_changeset_uuid = $previous_changeset_uuid;
3619
3620 /*
3621 * Convert all autosave revisions into their own auto-drafts so that users can be prompted to
3622 * restore them when a changeset is published, but they had been locked out from including
3623 * their changes in the changeset.
3624 */
3625 $revisions = wp_get_post_revisions( $changeset_post_id, array( 'check_enabled' => false ) );
3626 foreach ( $revisions as $revision ) {
3627 if ( str_contains( $revision->post_name, "{$changeset_post_id}-autosave" ) ) {
3628 $wpdb->update(
3629 $wpdb->posts,
3630 array(
3631 'post_status' => 'auto-draft',
3632 'post_type' => 'customize_changeset',
3633 'post_name' => wp_generate_uuid4(),
3634 'post_parent' => 0,
3635 ),
3636 array(
3637 'ID' => $revision->ID,
3638 )
3639 );
3640 clean_post_cache( $revision->ID );
3641 }
3642 }
3643
3644 return true;
3645 }
3646
3647 /**
3648 * Updates stashed theme mod settings.
3649 *
3650 * @since 4.7.0
3651 *
3652 * @param array $inactive_theme_mod_settings Mapping of stylesheet to arrays of theme mod settings.
3653 * @return array|false Returns array of updated stashed theme mods or false if the update failed or there were no changes.
3654 */
3655 protected function update_stashed_theme_mod_settings( $inactive_theme_mod_settings ) {
3656 $stashed_theme_mod_settings = get_option( 'customize_stashed_theme_mods' );
3657 if ( empty( $stashed_theme_mod_settings ) ) {
3658 $stashed_theme_mod_settings = array();
3659 }
3660
3661 // Delete any stashed theme mods for the active theme since they would have been loaded and saved upon activation.
3662 unset( $stashed_theme_mod_settings[ $this->get_stylesheet() ] );
3663
3664 // Merge inactive theme mods with the stashed theme mod settings.
3665 foreach ( $inactive_theme_mod_settings as $stylesheet => $theme_mod_settings ) {
3666 if ( ! isset( $stashed_theme_mod_settings[ $stylesheet ] ) ) {
3667 $stashed_theme_mod_settings[ $stylesheet ] = array();
3668 }
3669
3670 $stashed_theme_mod_settings[ $stylesheet ] = array_merge(
3671 $stashed_theme_mod_settings[ $stylesheet ],
3672 $theme_mod_settings
3673 );
3674 }
3675
3676 $autoload = false;
3677 $result = update_option( 'customize_stashed_theme_mods', $stashed_theme_mod_settings, $autoload );
3678 if ( ! $result ) {
3679 return false;
3680 }
3681 return $stashed_theme_mod_settings;
3682 }
3683
3684 /**
3685 * Refreshes nonces for the current preview.
3686 *
3687 * @since 4.2.0
3688 */
3689 public function refresh_nonces() {
3690 if ( ! $this->is_preview() ) {
3691 wp_send_json_error( 'not_preview' );
3692 }
3693
3694 wp_send_json_success( $this->get_nonces() );
3695 }
3696
3697 /**
3698 * Deletes a given auto-draft changeset or the autosave revision for a given changeset or delete changeset lock.
3699 *
3700 * @since 4.9.0
3701 */
3702 public function handle_dismiss_autosave_or_lock_request() {
3703 // Calls to dismiss_user_auto_draft_changesets() and wp_get_post_autosave() require non-zero get_current_user_id().
3704 if ( ! is_user_logged_in() ) {
3705 wp_send_json_error( 'unauthenticated', 401 );
3706 }
3707
3708 if ( ! $this->is_preview() ) {
3709 wp_send_json_error( 'not_preview', 400 );
3710 }
3711
3712 if ( ! check_ajax_referer( 'customize_dismiss_autosave_or_lock', 'nonce', false ) ) {
3713 wp_send_json_error( 'invalid_nonce', 403 );
3714 }
3715
3716 $changeset_post_id = $this->changeset_post_id();
3717 $dismiss_lock = ! empty( $_POST['dismiss_lock'] );
3718 $dismiss_autosave = ! empty( $_POST['dismiss_autosave'] );
3719
3720 if ( $dismiss_lock ) {
3721 if ( empty( $changeset_post_id ) && ! $dismiss_autosave ) {
3722 wp_send_json_error( 'no_changeset_to_dismiss_lock', 404 );
3723 }
3724 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post_id ) && ! $dismiss_autosave ) {
3725 wp_send_json_error( 'cannot_remove_changeset_lock', 403 );
3726 }
3727
3728 delete_post_meta( $changeset_post_id, '_edit_lock' );
3729
3730 if ( ! $dismiss_autosave ) {
3731 wp_send_json_success( 'changeset_lock_dismissed' );
3732 }
3733 }
3734
3735 if ( $dismiss_autosave ) {
3736 if ( empty( $changeset_post_id ) || 'auto-draft' === get_post_status( $changeset_post_id ) ) {
3737 $dismissed = $this->dismiss_user_auto_draft_changesets();
3738 if ( $dismissed > 0 ) {
3739 wp_send_json_success( 'auto_draft_dismissed' );
3740 } else {
3741 wp_send_json_error( 'no_auto_draft_to_delete', 404 );
3742 }
3743 } else {
3744 $revision = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
3745
3746 if ( $revision ) {
3747 if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
3748 wp_send_json_error( 'cannot_delete_autosave_revision', 403 );
3749 }
3750
3751 if ( ! wp_delete_post( $revision->ID, true ) ) {
3752 wp_send_json_error( 'autosave_revision_deletion_failure', 500 );
3753 } else {
3754 wp_send_json_success( 'autosave_revision_deleted' );
3755 }
3756 } else {
3757 wp_send_json_error( 'no_autosave_revision_to_delete', 404 );
3758 }
3759 }
3760 }
3761
3762 wp_send_json_error( 'unknown_error', 500 );
3763 }
3764
3765 /**
3766 * Adds a customize setting.
3767 *
3768 * @since 3.4.0
3769 * @since 4.5.0 Return added WP_Customize_Setting instance.
3770 *
3771 * @see WP_Customize_Setting::__construct()
3772 * @link https://developer.wordpress.org/themes/customize-api
3773 *
3774 * @param WP_Customize_Setting|string $id Customize Setting object, or ID.
3775 * @param array $args Optional. Array of properties for the new Setting object.
3776 * See WP_Customize_Setting::__construct() for information
3777 * on accepted arguments. Default empty array.
3778 * @return WP_Customize_Setting The instance of the setting that was added.
3779 */
3780 public function add_setting( $id, $args = array() ) {
3781 if ( $id instanceof WP_Customize_Setting ) {
3782 $setting = $id;
3783 } else {
3784 $class = 'WP_Customize_Setting';
3785
3786 /** This filter is documented in wp-includes/class-wp-customize-manager.php */
3787 $args = apply_filters( 'customize_dynamic_setting_args', $args, $id );
3788
3789 /** This filter is documented in wp-includes/class-wp-customize-manager.php */
3790 $class = apply_filters( 'customize_dynamic_setting_class', $class, $id, $args );
3791
3792 $setting = new $class( $this, $id, $args );
3793 }
3794
3795 $this->settings[ $setting->id ] = $setting;
3796 return $setting;
3797 }
3798
3799 /**
3800 * Registers any dynamically-created settings, such as those from $_POST['customized']
3801 * that have no corresponding setting created.
3802 *
3803 * This is a mechanism to "wake up" settings that have been dynamically created
3804 * on the front end and have been sent to WordPress in `$_POST['customized']`. When WP
3805 * loads, the dynamically-created settings then will get created and previewed
3806 * even though they are not directly created statically with code.
3807 *
3808 * @since 4.2.0
3809 *
3810 * @param array $setting_ids The setting IDs to add.
3811 * @return array The WP_Customize_Setting objects added.
3812 */
3813 public function add_dynamic_settings( $setting_ids ) {
3814 $new_settings = array();
3815 foreach ( $setting_ids as $setting_id ) {
3816 // Skip settings already created.
3817 if ( $this->get_setting( $setting_id ) ) {
3818 continue;
3819 }
3820
3821 $setting_args = false;
3822 $setting_class = 'WP_Customize_Setting';
3823
3824 /**
3825 * Filters a dynamic setting's constructor args.
3826 *
3827 * For a dynamic setting to be registered, this filter must be employed
3828 * to override the default false value with an array of args to pass to
3829 * the WP_Customize_Setting constructor.
3830 *
3831 * @since 4.2.0
3832 *
3833 * @param false|array $setting_args The arguments to the WP_Customize_Setting constructor.
3834 * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
3835 */
3836 $setting_args = apply_filters( 'customize_dynamic_setting_args', $setting_args, $setting_id );
3837 if ( false === $setting_args ) {
3838 continue;
3839 }
3840
3841 /**
3842 * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass.
3843 *
3844 * @since 4.2.0
3845 *
3846 * @param string $setting_class WP_Customize_Setting or a subclass.
3847 * @param string $setting_id ID for dynamic setting, usually coming from `$_POST['customized']`.
3848 * @param array $setting_args WP_Customize_Setting or a subclass.
3849 */
3850 $setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id, $setting_args );
3851
3852 $setting = new $setting_class( $this, $setting_id, $setting_args );
3853
3854 $this->add_setting( $setting );
3855 $new_settings[] = $setting;
3856 }
3857 return $new_settings;
3858 }
3859
3860 /**
3861 * Retrieves a customize setting.
3862 *
3863 * @since 3.4.0
3864 *
3865 * @param string $id Customize Setting ID.
3866 * @return WP_Customize_Setting|void The setting, if set.
3867 */
3868 public function get_setting( $id ) {
3869 if ( isset( $this->settings[ $id ] ) ) {
3870 return $this->settings[ $id ];
3871 }
3872 }
3873
3874 /**
3875 * Removes a customize setting.
3876 *
3877 * Note that removing the setting doesn't destroy the WP_Customize_Setting instance or remove its filters.
3878 *
3879 * @since 3.4.0
3880 *
3881 * @param string $id Customize Setting ID.
3882 */
3883 public function remove_setting( $id ) {
3884 unset( $this->settings[ $id ] );
3885 }
3886
3887 /**
3888 * Adds a customize panel.
3889 *
3890 * @since 4.0.0
3891 * @since 4.5.0 Return added WP_Customize_Panel instance.
3892 *
3893 * @see WP_Customize_Panel::__construct()
3894 *
3895 * @param WP_Customize_Panel|string $id Customize Panel object, or ID.
3896 * @param array $args Optional. Array of properties for the new Panel object.
3897 * See WP_Customize_Panel::__construct() for information
3898 * on accepted arguments. Default empty array.
3899 * @return WP_Customize_Panel The instance of the panel that was added.
3900 */
3901 public function add_panel( $id, $args = array() ) {
3902 if ( $id instanceof WP_Customize_Panel ) {
3903 $panel = $id;
3904 } else {
3905 $panel = new WP_Customize_Panel( $this, $id, $args );
3906 }
3907
3908 $this->panels[ $panel->id ] = $panel;
3909 return $panel;
3910 }
3911
3912 /**
3913 * Retrieves a customize panel.
3914 *
3915 * @since 4.0.0
3916 *
3917 * @param string $id Panel ID to get.
3918 * @return WP_Customize_Panel|void Requested panel instance, if set.
3919 */
3920 public function get_panel( $id ) {
3921 if ( isset( $this->panels[ $id ] ) ) {
3922 return $this->panels[ $id ];
3923 }
3924 }
3925
3926 /**
3927 * Removes a customize panel.
3928 *
3929 * Note that removing the panel doesn't destroy the WP_Customize_Panel instance or remove its filters.
3930 *
3931 * @since 4.0.0
3932 *
3933 * @param string $id Panel ID to remove.
3934 */
3935 public function remove_panel( $id ) {
3936 // Removing core components this way is _doing_it_wrong().
3937 if ( in_array( $id, $this->components, true ) ) {
3938 _doing_it_wrong(
3939 __METHOD__,
3940 sprintf(
3941 /* translators: 1: Panel ID, 2: Link to 'customize_loaded_components' filter reference. */
3942 __( 'Removing %1$s manually will cause PHP warnings. Use the %2$s filter instead.' ),
3943 $id,
3944 sprintf(
3945 '<a href="%1$s">%2$s</a>',
3946 esc_url( 'https://developer.wordpress.org/reference/hooks/customize_loaded_components/' ),
3947 '<code>customize_loaded_components</code>'
3948 )
3949 ),
3950 '4.5.0'
3951 );
3952 }
3953 unset( $this->panels[ $id ] );
3954 }
3955
3956 /**
3957 * Registers a customize panel type.
3958 *
3959 * Registered types are eligible to be rendered via JS and created dynamically.
3960 *
3961 * @since 4.3.0
3962 *
3963 * @see WP_Customize_Panel
3964 *
3965 * @param string $panel Name of a custom panel which is a subclass of WP_Customize_Panel.
3966 */
3967 public function register_panel_type( $panel ) {
3968 $this->registered_panel_types[] = $panel;
3969 }
3970
3971 /**
3972 * Renders JS templates for all registered panel types.
3973 *
3974 * @since 4.3.0
3975 */
3976 public function render_panel_templates() {
3977 foreach ( $this->registered_panel_types as $panel_type ) {
3978 $panel = new $panel_type( $this, 'temp', array() );
3979 $panel->print_template();
3980 }
3981 }
3982
3983 /**
3984 * Adds a customize section.
3985 *
3986 * @since 3.4.0
3987 * @since 4.5.0 Return added WP_Customize_Section instance.
3988 *
3989 * @see WP_Customize_Section::__construct()
3990 *
3991 * @param WP_Customize_Section|string $id Customize Section object, or ID.
3992 * @param array $args Optional. Array of properties for the new Section object.
3993 * See WP_Customize_Section::__construct() for information
3994 * on accepted arguments. Default empty array.
3995 * @return WP_Customize_Section The instance of the section that was added.
3996 */
3997 public function add_section( $id, $args = array() ) {
3998 if ( $id instanceof WP_Customize_Section ) {
3999 $section = $id;
4000 } else {
4001 $section = new WP_Customize_Section( $this, $id, $args );
4002 }
4003
4004 $this->sections[ $section->id ] = $section;
4005 return $section;
4006 }
4007
4008 /**
4009 * Retrieves a customize section.
4010 *
4011 * @since 3.4.0
4012 *
4013 * @param string $id Section ID.
4014 * @return WP_Customize_Section|void The section, if set.
4015 */
4016 public function get_section( $id ) {
4017 if ( isset( $this->sections[ $id ] ) ) {
4018 return $this->sections[ $id ];
4019 }
4020 }
4021
4022 /**
4023 * Removes a customize section.
4024 *
4025 * Note that removing the section doesn't destroy the WP_Customize_Section instance or remove its filters.
4026 *
4027 * @since 3.4.0
4028 *
4029 * @param string $id Section ID.
4030 */
4031 public function remove_section( $id ) {
4032 unset( $this->sections[ $id ] );
4033 }
4034
4035 /**
4036 * Registers a customize section type.
4037 *
4038 * Registered types are eligible to be rendered via JS and created dynamically.
4039 *
4040 * @since 4.3.0
4041 *
4042 * @see WP_Customize_Section
4043 *
4044 * @param string $section Name of a custom section which is a subclass of WP_Customize_Section.
4045 */
4046 public function register_section_type( $section ) {
4047 $this->registered_section_types[] = $section;
4048 }
4049
4050 /**
4051 * Renders JS templates for all registered section types.
4052 *
4053 * @since 4.3.0
4054 */
4055 public function render_section_templates() {
4056 foreach ( $this->registered_section_types as $section_type ) {
4057 $section = new $section_type( $this, 'temp', array() );
4058 $section->print_template();
4059 }
4060 }
4061
4062 /**
4063 * Adds a customize control.
4064 *
4065 * @since 3.4.0
4066 * @since 4.5.0 Return added WP_Customize_Control instance.
4067 *
4068 * @see WP_Customize_Control::__construct()
4069 *
4070 * @param WP_Customize_Control|string $id Customize Control object, or ID.
4071 * @param array $args Optional. Array of properties for the new Control object.
4072 * See WP_Customize_Control::__construct() for information
4073 * on accepted arguments. Default empty array.
4074 * @return WP_Customize_Control The instance of the control that was added.
4075 */
4076 public function add_control( $id, $args = array() ) {
4077 if ( $id instanceof WP_Customize_Control ) {
4078 $control = $id;
4079 } else {
4080 $control = new WP_Customize_Control( $this, $id, $args );
4081 }
4082
4083 $this->controls[ $control->id ] = $control;
4084 return $control;
4085 }
4086
4087 /**
4088 * Retrieves a customize control.
4089 *
4090 * @since 3.4.0
4091 *
4092 * @param string $id ID of the control.
4093 * @return WP_Customize_Control|void The control object, if set.
4094 */
4095 public function get_control( $id ) {
4096 if ( isset( $this->controls[ $id ] ) ) {
4097 return $this->controls[ $id ];
4098 }
4099 }
4100
4101 /**
4102 * Removes a customize control.
4103 *
4104 * Note that removing the control doesn't destroy the WP_Customize_Control instance or remove its filters.
4105 *
4106 * @since 3.4.0
4107 *
4108 * @param string $id ID of the control.
4109 */
4110 public function remove_control( $id ) {
4111 unset( $this->controls[ $id ] );
4112 }
4113
4114 /**
4115 * Registers a customize control type.
4116 *
4117 * Registered types are eligible to be rendered via JS and created dynamically.
4118 *
4119 * @since 4.1.0
4120 *
4121 * @param string $control Name of a custom control which is a subclass of
4122 * WP_Customize_Control.
4123 */
4124 public function register_control_type( $control ) {
4125 $this->registered_control_types[] = $control;
4126 }
4127
4128 /**
4129 * Renders JS templates for all registered control types.
4130 *
4131 * @since 4.1.0
4132 */
4133 public function render_control_templates() {
4134 if ( $this->branching() ) {
4135 $l10n = array(
4136 /* translators: %s: User who is customizing the changeset in customizer. */
4137 'locked' => __( '%s is already customizing this changeset. Please wait until they are done to try customizing. Your latest changes have been autosaved.' ),
4138 /* translators: %s: User who is customizing the changeset in customizer. */
4139 'locked_allow_override' => __( '%s is already customizing this changeset. Do you want to take over?' ),
4140 );
4141 } else {
4142 $l10n = array(
4143 /* translators: %s: User who is customizing the changeset in customizer. */
4144 'locked' => __( '%s is already customizing this site. Please wait until they are done to try customizing. Your latest changes have been autosaved.' ),
4145 /* translators: %s: User who is customizing the changeset in customizer. */
4146 'locked_allow_override' => __( '%s is already customizing this site. Do you want to take over?' ),
4147 );
4148 }
4149
4150 foreach ( $this->registered_control_types as $control_type ) {
4151 $control = new $control_type(
4152 $this,
4153 'temp',
4154 array(
4155 'settings' => array(),
4156 )
4157 );
4158 $control->print_template();
4159 }
4160 ?>
4161
4162 <script type="text/html" id="tmpl-customize-control-default-content">
4163 <#
4164 var inputId = _.uniqueId( 'customize-control-default-input-' );
4165 var descriptionId = _.uniqueId( 'customize-control-default-description-' );
4166 var describedByAttr = data.description ? ' aria-describedby="' + descriptionId + '" ' : '';
4167 #>
4168 <# switch ( data.type ) {
4169 case 'checkbox': #>
4170 <span class="customize-inside-control-row">
4171 <input
4172 id="{{ inputId }}"
4173 {{{ describedByAttr }}}
4174 type="checkbox"
4175 value="{{ data.value }}"
4176 data-customize-setting-key-link="default"
4177 >
4178 <label for="{{ inputId }}">
4179 {{ data.label }}
4180 </label>
4181 <# if ( data.description ) { #>
4182 <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
4183 <# } #>
4184 </span>
4185 <#
4186 break;
4187 case 'radio':
4188 if ( ! data.choices ) {
4189 return;
4190 }
4191 #>
4192 <# if ( data.label ) { #>
4193 <label for="{{ inputId }}" class="customize-control-title">
4194 {{ data.label }}
4195 </label>
4196 <# } #>
4197 <# if ( data.description ) { #>
4198 <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
4199 <# } #>
4200 <# _.each( data.choices, function( val, key ) { #>
4201 <span class="customize-inside-control-row">
4202 <#
4203 var value, text;
4204 if ( _.isObject( val ) ) {
4205 value = val.value;
4206 text = val.text;
4207 } else {
4208 value = key;
4209 text = val;
4210 }
4211 #>
4212 <input
4213 id="{{ inputId + '-' + value }}"
4214 type="radio"
4215 value="{{ value }}"
4216 name="{{ inputId }}"
4217 data-customize-setting-key-link="default"
4218 {{{ describedByAttr }}}
4219 >
4220 <label for="{{ inputId + '-' + value }}">{{ text }}</label>
4221 </span>
4222 <# } ); #>
4223 <#
4224 break;
4225 default:
4226 #>
4227 <# if ( data.label ) { #>
4228 <label for="{{ inputId }}" class="customize-control-title">
4229 {{ data.label }}
4230 </label>
4231 <# } #>
4232 <# if ( data.description ) { #>
4233 <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
4234 <# } #>
4235
4236 <#
4237 var inputAttrs = {
4238 id: inputId,
4239 'data-customize-setting-key-link': 'default'
4240 };
4241 if ( 'textarea' === data.type ) {
4242 inputAttrs.rows = '5';
4243 } else if ( 'button' === data.type ) {
4244 inputAttrs['class'] = 'button button-secondary';
4245 inputAttrs.type = 'button';
4246 } else {
4247 inputAttrs.type = data.type;
4248 }
4249 if ( data.description ) {
4250 inputAttrs['aria-describedby'] = descriptionId;
4251 }
4252 _.extend( inputAttrs, data.input_attrs );
4253 #>
4254
4255 <# if ( 'button' === data.type ) { #>
4256 <button
4257 <# _.each( _.extend( inputAttrs ), function( value, key ) { #>
4258 {{{ key }}}="{{ value }}"
4259 <# } ); #>
4260 >{{ inputAttrs.value }}</button>
4261 <# } else if ( 'textarea' === data.type ) { #>
4262 <textarea
4263 <# _.each( _.extend( inputAttrs ), function( value, key ) { #>
4264 {{{ key }}}="{{ value }}"
4265 <# }); #>
4266 >{{ inputAttrs.value }}</textarea>
4267 <# } else if ( 'select' === data.type ) { #>
4268 <# delete inputAttrs.type; #>
4269 <select
4270 <# _.each( _.extend( inputAttrs ), function( value, key ) { #>
4271 {{{ key }}}="{{ value }}"
4272 <# }); #>
4273 >
4274 <# _.each( data.choices, function( val, key ) { #>
4275 <#
4276 var value, text;
4277 if ( _.isObject( val ) ) {
4278 value = val.value;
4279 text = val.text;
4280 } else {
4281 value = key;
4282 text = val;
4283 }
4284 #>
4285 <option value="{{ value }}">{{ text }}</option>
4286 <# } ); #>
4287 </select>
4288 <# } else { #>
4289 <input
4290 <# _.each( _.extend( inputAttrs ), function( value, key ) { #>
4291 {{{ key }}}="{{ value }}"
4292 <# }); #>
4293 >
4294 <# } #>
4295 <# } #>
4296 </script>
4297
4298 <script type="text/html" id="tmpl-customize-notification">
4299 <li class="notice notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
4300 <div class="notification-message">{{{ data.message || data.code }}}</div>
4301 <# if ( data.dismissible ) { #>
4302 <button type="button" class="notice-dismiss"><span class="screen-reader-text">
4303 <?php
4304 /* translators: Hidden accessibility text. */
4305 _e( 'Dismiss' );
4306 ?>
4307 </span></button>
4308 <# } #>
4309 </li>
4310 </script>
4311
4312 <script type="text/html" id="tmpl-customize-changeset-locked-notification">
4313 <li class="notice notice-{{ data.type || 'info' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
4314 <div class="notification-message customize-changeset-locked-message {{ data.lockUser.avatar ? 'has-avatar' : '' }}">
4315 <# if ( data.lockUser.avatar ) { #>
4316 <img class="customize-changeset-locked-avatar" src="{{ data.lockUser.avatar }}" alt="{{ data.lockUser.name }}" />
4317 <# } #>
4318 <p class="currently-editing">
4319 <# if ( data.message ) { #>
4320 {{{ data.message }}}
4321 <# } else if ( data.allowOverride ) { #>
4322 <?php
4323 echo esc_html( sprintf( $l10n['locked_allow_override'], '{{ data.lockUser.name }}' ) );
4324 ?>
4325 <# } else { #>
4326 <?php
4327 echo esc_html( sprintf( $l10n['locked'], '{{ data.lockUser.name }}' ) );
4328 ?>
4329 <# } #>
4330 </p>
4331 <p class="notice notice-error notice-alt" hidden></p>
4332 <p class="action-buttons">
4333 <# if ( data.returnUrl !== data.previewUrl ) { #>
4334 <a class="button customize-notice-go-back-button" href="{{ data.returnUrl }}"><?php _e( 'Go back' ); ?></a>
4335 <# } #>
4336 <a class="button customize-notice-preview-button" href="{{ data.frontendPreviewUrl }}"><?php _e( 'Preview' ); ?></a>
4337 <# if ( data.allowOverride ) { #>
4338 <button class="button button-primary wp-tab-last customize-notice-take-over-button"><?php _e( 'Take over' ); ?></button>
4339 <# } #>
4340 </p>
4341 </div>
4342 </li>
4343 </script>
4344
4345 <script type="text/html" id="tmpl-customize-code-editor-lint-error-notification">
4346 <li class="notice notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }} {{ data.containerClasses || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
4347 <div class="notification-message">{{{ data.message || data.code }}}</div>
4348
4349 <p>
4350 <# var elementId = 'el-' + String( Math.random() ); #>
4351 <input id="{{ elementId }}" type="checkbox">
4352 <label for="{{ elementId }}"><?php _e( 'Update anyway, even though it might break your site?' ); ?></label>
4353 </p>
4354 </li>
4355 </script>
4356
4357 <?php
4358 /* The following template is obsolete in core but retained for plugins. */
4359 ?>
4360 <script type="text/html" id="tmpl-customize-control-notifications">
4361 <ul>
4362 <# _.each( data.notifications, function( notification ) { #>
4363 <li class="notice notice-{{ notification.type || 'info' }} {{ data.altNotice ? 'notice-alt' : '' }}" data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{{ notification.message || notification.code }}}</li>
4364 <# } ); #>
4365 </ul>
4366 </script>
4367
4368 <script type="text/html" id="tmpl-customize-preview-link-control" >
4369 <# var elementPrefix = _.uniqueId( 'el' ) + '-' #>
4370 <p class="customize-control-title">
4371 <?php esc_html_e( 'Share Preview Link' ); ?>
4372 </p>
4373 <p class="description customize-control-description"><?php esc_html_e( 'See how changes would look live on your website, and share the preview with people who can\'t access the Customizer.' ); ?></p>
4374 <div class="customize-control-notifications-container"></div>
4375 <div class="preview-link-wrapper">
4376 <label for="{{ elementPrefix }}customize-preview-link-input" class="screen-reader-text">
4377 <?php
4378 /* translators: Hidden accessibility text. */
4379 esc_html_e( 'Preview Link' );
4380 ?>
4381 </label>
4382 <a href="" target="">
4383 <span class="preview-control-element" data-component="url"></span>
4384 <span class="screen-reader-text">
4385 <?php
4386 /* translators: Hidden accessibility text. */
4387 _e( '(opens in a new tab)' );
4388 ?>
4389 </span>
4390 </a>
4391 <input id="{{ elementPrefix }}customize-preview-link-input" readonly tabindex="-1" class="preview-control-element" data-component="input">
4392 <button class="customize-copy-preview-link preview-control-element button button-secondary" data-component="button" data-copy-text="<?php esc_attr_e( 'Copy' ); ?>" data-copied-text="<?php esc_attr_e( 'Copied' ); ?>" ><?php esc_html_e( 'Copy' ); ?></button>
4393 </div>
4394 </script>
4395 <script type="text/html" id="tmpl-customize-selected-changeset-status-control">
4396 <# var inputId = _.uniqueId( 'customize-selected-changeset-status-control-input-' ); #>
4397 <# var descriptionId = _.uniqueId( 'customize-selected-changeset-status-control-description-' ); #>
4398 <# if ( data.label ) { #>
4399 <label for="{{ inputId }}" class="customize-control-title">{{ data.label }}</label>
4400 <# } #>
4401 <# if ( data.description ) { #>
4402 <span id="{{ descriptionId }}" class="description customize-control-description">{{{ data.description }}}</span>
4403 <# } #>
4404 <# _.each( data.choices, function( choice ) { #>
4405 <# var choiceId = inputId + '-' + choice.status; #>
4406 <span class="customize-inside-control-row">
4407 <input id="{{ choiceId }}" type="radio" value="{{ choice.status }}" name="{{ inputId }}" data-customize-setting-key-link="default">
4408 <label for="{{ choiceId }}">{{ choice.label }}</label>
4409 </span>
4410 <# } ); #>
4411 </script>
4412 <?php
4413 }
4414
4415 /**
4416 * Helper function to compare two objects by priority, ensuring sort stability via instance_number.
4417 *
4418 * @since 3.4.0
4419 * @deprecated 4.7.0 Use wp_list_sort()
4420 *
4421 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $a Object A.
4422 * @param WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control $b Object B.
4423 * @return int
4424 */
4425 protected function _cmp_priority( $a, $b ) {
4426 _deprecated_function( __METHOD__, '4.7.0', 'wp_list_sort' );
4427
4428 if ( $a->priority === $b->priority ) {
4429 return $a->instance_number - $b->instance_number;
4430 } else {
4431 return $a->priority - $b->priority;
4432 }
4433 }
4434
4435 /**
4436 * Prepares panels, sections, and controls.
4437 *
4438 * For each, check if required related components exist,
4439 * whether the user has the necessary capabilities,
4440 * and sort by priority.
4441 *
4442 * @since 3.4.0
4443 */
4444 public function prepare_controls() {
4445
4446 $controls = array();
4447 $this->controls = wp_list_sort(
4448 $this->controls,
4449 array(
4450 'priority' => 'ASC',
4451 'instance_number' => 'ASC',
4452 ),
4453 'ASC',
4454 true
4455 );
4456
4457 foreach ( $this->controls as $id => $control ) {
4458 if ( ! isset( $this->sections[ $control->section ] ) || ! $control->check_capabilities() ) {
4459 continue;
4460 }
4461
4462 $this->sections[ $control->section ]->controls[] = $control;
4463 $controls[ $id ] = $control;
4464 }
4465 $this->controls = $controls;
4466
4467 // Prepare sections.
4468 $this->sections = wp_list_sort(
4469 $this->sections,
4470 array(
4471 'priority' => 'ASC',
4472 'instance_number' => 'ASC',
4473 ),
4474 'ASC',
4475 true
4476 );
4477 $sections = array();
4478
4479 foreach ( $this->sections as $section ) {
4480 if ( ! $section->check_capabilities() ) {
4481 continue;
4482 }
4483
4484 $section->controls = wp_list_sort(
4485 $section->controls,
4486 array(
4487 'priority' => 'ASC',
4488 'instance_number' => 'ASC',
4489 )
4490 );
4491
4492 if ( ! $section->panel ) {
4493 // Top-level section.
4494 $sections[ $section->id ] = $section;
4495 } else {
4496 // This section belongs to a panel.
4497 if ( isset( $this->panels [ $section->panel ] ) ) {
4498 $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
4499 }
4500 }
4501 }
4502 $this->sections = $sections;
4503
4504 // Prepare panels.
4505 $this->panels = wp_list_sort(
4506 $this->panels,
4507 array(
4508 'priority' => 'ASC',
4509 'instance_number' => 'ASC',
4510 ),
4511 'ASC',
4512 true
4513 );
4514 $panels = array();
4515
4516 foreach ( $this->panels as $panel ) {
4517 if ( ! $panel->check_capabilities() ) {
4518 continue;
4519 }
4520
4521 $panel->sections = wp_list_sort(
4522 $panel->sections,
4523 array(
4524 'priority' => 'ASC',
4525 'instance_number' => 'ASC',
4526 ),
4527 'ASC',
4528 true
4529 );
4530 $panels[ $panel->id ] = $panel;
4531 }
4532 $this->panels = $panels;
4533
4534 // Sort panels and top-level sections together.
4535 $this->containers = array_merge( $this->panels, $this->sections );
4536 $this->containers = wp_list_sort(
4537 $this->containers,
4538 array(
4539 'priority' => 'ASC',
4540 'instance_number' => 'ASC',
4541 ),
4542 'ASC',
4543 true
4544 );
4545 }
4546
4547 /**
4548 * Enqueues scripts for customize controls.
4549 *
4550 * @since 3.4.0
4551 */
4552 public function enqueue_control_scripts() {
4553 foreach ( $this->controls as $control ) {
4554 $control->enqueue();
4555 }
4556
4557 if ( ! is_multisite() && ( current_user_can( 'install_themes' ) || current_user_can( 'update_themes' ) || current_user_can( 'delete_themes' ) ) ) {
4558 wp_enqueue_script( 'updates' );
4559 wp_localize_script(
4560 'updates',
4561 '_wpUpdatesItemCounts',
4562 array(
4563 'totals' => wp_get_update_data(),
4564 )
4565 );
4566 }
4567 }
4568
4569 /**
4570 * Determines whether the user agent is iOS.
4571 *
4572 * @since 4.4.0
4573 *
4574 * @return bool Whether the user agent is iOS.
4575 */
4576 public function is_ios() {
4577 return wp_is_mobile() && preg_match( '/iPad|iPod|iPhone/', $_SERVER['HTTP_USER_AGENT'] );
4578 }
4579
4580 /**
4581 * Gets the template string for the Customizer pane document title.
4582 *
4583 * @since 4.4.0
4584 *
4585 * @return string The template string for the document title.
4586 */
4587 public function get_document_title_template() {
4588 if ( $this->is_theme_active() ) {
4589 /* translators: %s: Document title from the preview. */
4590 $document_title_tmpl = __( 'Customize: %s' );
4591 } else {
4592 /* translators: %s: Document title from the preview. */
4593 $document_title_tmpl = __( 'Live Preview: %s' );
4594 }
4595 $document_title_tmpl = html_entity_decode( $document_title_tmpl, ENT_QUOTES, 'UTF-8' ); // Because exported to JS and assigned to document.title.
4596 return $document_title_tmpl;
4597 }
4598
4599 /**
4600 * Sets the initial URL to be previewed.
4601 *
4602 * URL is validated.
4603 *
4604 * @since 4.4.0
4605 *
4606 * @param string $preview_url URL to be previewed.
4607 */
4608 public function set_preview_url( $preview_url ) {
4609 $preview_url = sanitize_url( $preview_url );
4610 $this->preview_url = wp_validate_redirect( $preview_url, home_url( '/' ) );
4611 }
4612
4613 /**
4614 * Gets the initial URL to be previewed.
4615 *
4616 * @since 4.4.0
4617 *
4618 * @return string URL being previewed.
4619 */
4620 public function get_preview_url() {
4621 if ( empty( $this->preview_url ) ) {
4622 $preview_url = home_url( '/' );
4623 } else {
4624 $preview_url = $this->preview_url;
4625 }
4626 return $preview_url;
4627 }
4628
4629 /**
4630 * Determines whether the admin and the frontend are on different domains.
4631 *
4632 * @since 4.7.0
4633 *
4634 * @return bool Whether cross-domain.
4635 */
4636 public function is_cross_domain() {
4637 $admin_origin = wp_parse_url( admin_url() );
4638 $home_origin = wp_parse_url( home_url() );
4639 $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
4640 return $cross_domain;
4641 }
4642
4643 /**
4644 * Gets URLs allowed to be previewed.
4645 *
4646 * If the front end and the admin are served from the same domain, load the
4647 * preview over ssl if the Customizer is being loaded over ssl. This avoids
4648 * insecure content warnings. This is not attempted if the admin and front end
4649 * are on different domains to avoid the case where the front end doesn't have
4650 * ssl certs. Domain mapping plugins can allow other urls in these conditions
4651 * using the customize_allowed_urls filter.
4652 *
4653 * @since 4.7.0
4654 *
4655 * @return array Allowed URLs.
4656 */
4657 public function get_allowed_urls() {
4658 $allowed_urls = array( home_url( '/' ) );
4659
4660 if ( is_ssl() && ! $this->is_cross_domain() ) {
4661 $allowed_urls[] = home_url( '/', 'https' );
4662 }
4663
4664 /**
4665 * Filters the list of URLs allowed to be clicked and followed in the Customizer preview.
4666 *
4667 * @since 3.4.0
4668 *
4669 * @param string[] $allowed_urls An array of allowed URLs.
4670 */
4671 $allowed_urls = array_unique( apply_filters( 'customize_allowed_urls', $allowed_urls ) );
4672
4673 return $allowed_urls;
4674 }
4675
4676 /**
4677 * Gets messenger channel.
4678 *
4679 * @since 4.7.0
4680 *
4681 * @return string Messenger channel.
4682 */
4683 public function get_messenger_channel() {
4684 return $this->messenger_channel;
4685 }
4686
4687 /**
4688 * Sets URL to link the user to when closing the Customizer.
4689 *
4690 * URL is validated.
4691 *
4692 * @since 4.4.0
4693 *
4694 * @param string $return_url URL for return link.
4695 */
4696 public function set_return_url( $return_url ) {
4697 $return_url = sanitize_url( $return_url );
4698 $return_url = remove_query_arg( wp_removable_query_args(), $return_url );
4699 $return_url = wp_validate_redirect( $return_url );
4700 $this->return_url = $return_url;
4701 }
4702
4703 /**
4704 * Gets URL to link the user to when closing the Customizer.
4705 *
4706 * @since 4.4.0
4707 *
4708 * @global array $_registered_pages
4709 *
4710 * @return string URL for link to close Customizer.
4711 */
4712 public function get_return_url() {
4713 global $_registered_pages;
4714
4715 $referer = wp_get_referer();
4716 $excluded_referer_basenames = array( 'customize.php', 'wp-login.php' );
4717
4718 if ( $this->return_url ) {
4719 $return_url = $this->return_url;
4720
4721 $return_url_basename = wp_basename( parse_url( $this->return_url, PHP_URL_PATH ) );
4722 $return_url_query = parse_url( $this->return_url, PHP_URL_QUERY );
4723
4724 if ( 'themes.php' === $return_url_basename && $return_url_query ) {
4725 parse_str( $return_url_query, $query_vars );
4726
4727 /*
4728 * If the return URL is a page added by a theme to the Appearance menu via add_submenu_page(),
4729 * verify that it belongs to the active theme, otherwise fall back to the Themes screen.
4730 */
4731 if ( isset( $query_vars['page'] ) && ! isset( $_registered_pages[ "appearance_page_{$query_vars['page']}" ] ) ) {
4732 $return_url = admin_url( 'themes.php' );
4733 }
4734 }
4735 } elseif ( $referer && ! in_array( wp_basename( parse_url( $referer, PHP_URL_PATH ) ), $excluded_referer_basenames, true ) ) {
4736 $return_url = $referer;
4737 } elseif ( $this->preview_url ) {
4738 $return_url = $this->preview_url;
4739 } else {
4740 $return_url = home_url( '/' );
4741 }
4742
4743 return $return_url;
4744 }
4745
4746 /**
4747 * Sets the autofocused constructs.
4748 *
4749 * @since 4.4.0
4750 *
4751 * @param array $autofocus {
4752 * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
4753 *
4754 * @type string $control ID for control to be autofocused.
4755 * @type string $section ID for section to be autofocused.
4756 * @type string $panel ID for panel to be autofocused.
4757 * }
4758 */
4759 public function set_autofocus( $autofocus ) {
4760 $this->autofocus = array_filter( wp_array_slice_assoc( $autofocus, array( 'panel', 'section', 'control' ) ), 'is_string' );
4761 }
4762
4763 /**
4764 * Gets the autofocused constructs.
4765 *
4766 * @since 4.4.0
4767 *
4768 * @return string[] {
4769 * Mapping of 'panel', 'section', 'control' to the ID which should be autofocused.
4770 *
4771 * @type string $control ID for control to be autofocused.
4772 * @type string $section ID for section to be autofocused.
4773 * @type string $panel ID for panel to be autofocused.
4774 * }
4775 */
4776 public function get_autofocus() {
4777 return $this->autofocus;
4778 }
4779
4780 /**
4781 * Gets nonces for the Customizer.
4782 *
4783 * @since 4.5.0
4784 *
4785 * @return array Nonces.
4786 */
4787 public function get_nonces() {
4788 $nonces = array(
4789 'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
4790 'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
4791 'switch_themes' => wp_create_nonce( 'switch_themes' ),
4792 'dismiss_autosave_or_lock' => wp_create_nonce( 'customize_dismiss_autosave_or_lock' ),
4793 'override_lock' => wp_create_nonce( 'customize_override_changeset_lock' ),
4794 'trash' => wp_create_nonce( 'trash_customize_changeset' ),
4795 );
4796
4797 /**
4798 * Filters nonces for Customizer.
4799 *
4800 * @since 4.2.0
4801 *
4802 * @param string[] $nonces Array of refreshed nonces for save and
4803 * preview actions.
4804 * @param WP_Customize_Manager $manager WP_Customize_Manager instance.
4805 */
4806 $nonces = apply_filters( 'customize_refresh_nonces', $nonces, $this );
4807
4808 return $nonces;
4809 }
4810
4811 /**
4812 * Prints JavaScript settings for parent window.
4813 *
4814 * @since 4.4.0
4815 */
4816 public function customize_pane_settings() {
4817
4818 $login_url = add_query_arg(
4819 array(
4820 'interim-login' => 1,
4821 'customize-login' => 1,
4822 ),
4823 wp_login_url()
4824 );
4825
4826 // Ensure dirty flags are set for modified settings.
4827 foreach ( array_keys( $this->unsanitized_post_values() ) as $setting_id ) {
4828 $setting = $this->get_setting( $setting_id );
4829 if ( $setting ) {
4830 $setting->dirty = true;
4831 }
4832 }
4833
4834 $autosave_revision_post = null;
4835 $autosave_autodraft_post = null;
4836 $changeset_post_id = $this->changeset_post_id();
4837 if ( ! $this->saved_starter_content_changeset && ! $this->autosaved() ) {
4838 if ( $changeset_post_id ) {
4839 if ( is_user_logged_in() ) {
4840 $autosave_revision_post = wp_get_post_autosave( $changeset_post_id, get_current_user_id() );
4841 }
4842 } else {
4843 $autosave_autodraft_posts = $this->get_changeset_posts(
4844 array(
4845 'posts_per_page' => 1,
4846 'post_status' => 'auto-draft',
4847 'exclude_restore_dismissed' => true,
4848 )
4849 );
4850 if ( ! empty( $autosave_autodraft_posts ) ) {
4851 $autosave_autodraft_post = array_shift( $autosave_autodraft_posts );
4852 }
4853 }
4854 }
4855
4856 $current_user_can_publish = current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts );
4857
4858 // @todo Include all of the status labels here from script-loader.php, and then allow it to be filtered.
4859 $status_choices = array();
4860 if ( $current_user_can_publish ) {
4861 $status_choices[] = array(
4862 'status' => 'publish',
4863 'label' => __( 'Publish' ),
4864 );
4865 }
4866 $status_choices[] = array(
4867 'status' => 'draft',
4868 'label' => __( 'Save Draft' ),
4869 );
4870 if ( $current_user_can_publish ) {
4871 $status_choices[] = array(
4872 'status' => 'future',
4873 'label' => _x( 'Schedule', 'customizer changeset action/button label' ),
4874 );
4875 }
4876
4877 // Prepare Customizer settings to pass to JavaScript.
4878 $changeset_post = null;
4879 if ( $changeset_post_id ) {
4880 $changeset_post = get_post( $changeset_post_id );
4881 }
4882
4883 // Determine initial date to be at present or future, not past.
4884 $current_time = current_time( 'mysql', false );
4885 $initial_date = $current_time;
4886 if ( $changeset_post ) {
4887 $initial_date = get_the_time( 'Y-m-d H:i:s', $changeset_post->ID );
4888 if ( $initial_date < $current_time ) {
4889 $initial_date = $current_time;
4890 }
4891 }
4892
4893 $lock_user_id = false;
4894 if ( $this->changeset_post_id() ) {
4895 $lock_user_id = wp_check_post_lock( $this->changeset_post_id() );
4896 }
4897
4898 $settings = array(
4899 'changeset' => array(
4900 'uuid' => $this->changeset_uuid(),
4901 'branching' => $this->branching(),
4902 'autosaved' => $this->autosaved(),
4903 'hasAutosaveRevision' => ! empty( $autosave_revision_post ),
4904 'latestAutoDraftUuid' => $autosave_autodraft_post ? $autosave_autodraft_post->post_name : null,
4905 'status' => $changeset_post ? $changeset_post->post_status : '',
4906 'currentUserCanPublish' => $current_user_can_publish,
4907 'publishDate' => $initial_date,
4908 'statusChoices' => $status_choices,
4909 'lockUser' => $lock_user_id ? $this->get_lock_user_data( $lock_user_id ) : null,
4910 ),
4911 'initialServerDate' => $current_time,
4912 'dateFormat' => get_option( 'date_format' ),
4913 'timeFormat' => get_option( 'time_format' ),
4914 'initialServerTimestamp' => floor( microtime( true ) * 1000 ),
4915 'initialClientTimestamp' => -1, // To be set with JS below.
4916 'timeouts' => array(
4917 'windowRefresh' => 250,
4918 'changesetAutoSave' => AUTOSAVE_INTERVAL * 1000,
4919 'keepAliveCheck' => 2500,
4920 'reflowPaneContents' => 100,
4921 'previewFrameSensitivity' => 2000,
4922 ),
4923 'theme' => array(
4924 'stylesheet' => $this->get_stylesheet(),
4925 'active' => $this->is_theme_active(),
4926 '_canInstall' => current_user_can( 'install_themes' ),
4927 ),
4928 'url' => array(
4929 'preview' => sanitize_url( $this->get_preview_url() ),
4930 'return' => sanitize_url( $this->get_return_url() ),
4931 'parent' => sanitize_url( admin_url() ),
4932 'activated' => sanitize_url( home_url( '/' ) ),
4933 'ajax' => sanitize_url( admin_url( 'admin-ajax.php', 'relative' ) ),
4934 'allowed' => array_map( 'sanitize_url', $this->get_allowed_urls() ),
4935 'isCrossDomain' => $this->is_cross_domain(),
4936 'home' => sanitize_url( home_url( '/' ) ),
4937 'login' => sanitize_url( $login_url ),
4938 ),
4939 'browser' => array(
4940 'mobile' => wp_is_mobile(),
4941 'ios' => $this->is_ios(),
4942 ),
4943 'panels' => array(),
4944 'sections' => array(),
4945 'nonce' => $this->get_nonces(),
4946 'autofocus' => $this->get_autofocus(),
4947 'documentTitleTmpl' => $this->get_document_title_template(),
4948 'previewableDevices' => $this->get_previewable_devices(),
4949 'l10n' => array(
4950 'confirmDeleteTheme' => __( 'Are you sure you want to delete this theme?' ),
4951 /* translators: %d: Number of theme search results, which cannot currently consider singular vs. plural forms. */
4952 'themeSearchResults' => __( '%d themes found' ),
4953 /* translators: %d: Number of themes being displayed, which cannot currently consider singular vs. plural forms. */
4954 'announceThemeCount' => __( 'Displaying %d themes' ),
4955 /* translators: %s: Theme name. */
4956 'announceThemeDetails' => __( 'Showing details for theme: %s' ),
4957 ),
4958 );
4959
4960 // Temporarily disable installation in Customizer. See #42184.
4961 $filesystem_method = get_filesystem_method();
4962 ob_start();
4963 $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
4964 ob_end_clean();
4965 if ( 'direct' !== $filesystem_method && ! $filesystem_credentials_are_stored ) {
4966 $settings['theme']['_filesystemCredentialsNeeded'] = true;
4967 }
4968
4969 // Prepare Customize Section objects to pass to JavaScript.
4970 foreach ( $this->sections() as $id => $section ) {
4971 if ( $section->check_capabilities() ) {
4972 $settings['sections'][ $id ] = $section->json();
4973 }
4974 }
4975
4976 // Prepare Customize Panel objects to pass to JavaScript.
4977 foreach ( $this->panels() as $panel_id => $panel ) {
4978 if ( $panel->check_capabilities() ) {
4979 $settings['panels'][ $panel_id ] = $panel->json();
4980 foreach ( $panel->sections as $section_id => $section ) {
4981 if ( $section->check_capabilities() ) {
4982 $settings['sections'][ $section_id ] = $section->json();
4983 }
4984 }
4985 }
4986 }
4987
4988 ob_start();
4989 ?>
4990 <script>
4991 var _wpCustomizeSettings = <?php echo wp_json_encode( $settings, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ); ?>;
4992 _wpCustomizeSettings.initialClientTimestamp = _.now();
4993 _wpCustomizeSettings.controls = {};
4994 _wpCustomizeSettings.settings = {};
4995 <?php
4996
4997 // Serialize settings one by one to improve memory usage.
4998 echo "(function ( s ){\n";
4999 foreach ( $this->settings() as $setting ) {
5000 if ( $setting->check_capabilities() ) {
5001 printf(
5002 "s[%s] = %s;\n",
5003 wp_json_encode( $setting->id, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
5004 wp_json_encode( $setting->json(), JSON_HEX_TAG | JSON_UNESCAPED_SLASHES )
5005 );
5006 }
5007 }
5008 echo "})( _wpCustomizeSettings.settings );\n";
5009
5010 // Serialize controls one by one to improve memory usage.
5011 echo "(function ( c ){\n";
5012 foreach ( $this->controls() as $control ) {
5013 if ( $control->check_capabilities() ) {
5014 printf(
5015 "c[%s] = %s;\n",
5016 wp_json_encode( $control->id, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
5017 wp_json_encode( $control->json(), JSON_HEX_TAG | JSON_UNESCAPED_SLASHES )
5018 );
5019 }
5020 }
5021 echo "})( _wpCustomizeSettings.controls );\n";
5022 ?>
5023 </script>
5024 <?php
5025 wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) . "\n//# sourceURL=" . rawurlencode( __METHOD__ ) );
5026 }
5027
5028 /**
5029 * Returns a list of devices to allow previewing.
5030 *
5031 * @since 4.5.0
5032 *
5033 * @return array List of devices with labels and default setting.
5034 */
5035 public function get_previewable_devices() {
5036 $devices = array(
5037 'desktop' => array(
5038 'label' => __( 'Enter desktop preview mode' ),
5039 'default' => true,
5040 ),
5041 'tablet' => array(
5042 'label' => __( 'Enter tablet preview mode' ),
5043 ),
5044 'mobile' => array(
5045 'label' => __( 'Enter mobile preview mode' ),
5046 ),
5047 );
5048
5049 /**
5050 * Filters the available devices to allow previewing in the Customizer.
5051 *
5052 * @since 4.5.0
5053 *
5054 * @see WP_Customize_Manager::get_previewable_devices()
5055 *
5056 * @param array $devices List of devices with labels and default setting.
5057 */
5058 $devices = apply_filters( 'customize_previewable_devices', $devices );
5059
5060 return $devices;
5061 }
5062
5063 /**
5064 * Registers some default controls.
5065 *
5066 * @since 3.4.0
5067 */
5068 public function register_controls() {
5069
5070 /* Themes (controls are loaded via ajax) */
5071
5072 $this->add_panel(
5073 new WP_Customize_Themes_Panel(
5074 $this,
5075 'themes',
5076 array(
5077 'title' => $this->theme()->display( 'Name' ),
5078 'description' => (
5079 '<p>' . __( 'Looking for a theme? You can search or browse the WordPress.org theme directory, install and preview themes, then activate them right here.' ) . '</p>' .
5080 '<p>' . __( 'While previewing a new theme, you can continue to tailor things like widgets and menus, and explore theme-specific options.' ) . '</p>'
5081 ),
5082 'capability' => 'switch_themes',
5083 'priority' => 0,
5084 )
5085 )
5086 );
5087
5088 $this->add_section(
5089 new WP_Customize_Themes_Section(
5090 $this,
5091 'installed_themes',
5092 array(
5093 'title' => __( 'Installed themes' ),
5094 'action' => 'installed',
5095 'capability' => 'switch_themes',
5096 'panel' => 'themes',
5097 'priority' => 0,
5098 )
5099 )
5100 );
5101
5102 if ( ! is_multisite() ) {
5103 $this->add_section(
5104 new WP_Customize_Themes_Section(
5105 $this,
5106 'wporg_themes',
5107 array(
5108 'title' => __( 'WordPress.org themes' ),
5109 'action' => 'wporg',
5110 'filter_type' => 'remote',
5111 'capability' => 'install_themes',
5112 'panel' => 'themes',
5113 'priority' => 5,
5114 )
5115 )
5116 );
5117 }
5118
5119 // Themes Setting (unused - the theme is considerably more fundamental to the Customizer experience).
5120 $this->add_setting(
5121 new WP_Customize_Filter_Setting(
5122 $this,
5123 'active_theme',
5124 array(
5125 'capability' => 'switch_themes',
5126 )
5127 )
5128 );
5129
5130 /* Site Identity */
5131
5132 $this->add_section(
5133 'title_tagline',
5134 array(
5135 'title' => __( 'Site Identity' ),
5136 'priority' => 20,
5137 )
5138 );
5139
5140 $this->add_setting(
5141 'blogname',
5142 array(
5143 'default' => get_option( 'blogname' ),
5144 'type' => 'option',
5145 'capability' => 'manage_options',
5146 )
5147 );
5148
5149 $this->add_control(
5150 'blogname',
5151 array(
5152 'label' => __( 'Site Title' ),
5153 'section' => 'title_tagline',
5154 )
5155 );
5156
5157 $this->add_setting(
5158 'blogdescription',
5159 array(
5160 'default' => get_option( 'blogdescription' ),
5161 'type' => 'option',
5162 'capability' => 'manage_options',
5163 )
5164 );
5165
5166 $this->add_control(
5167 'blogdescription',
5168 array(
5169 'label' => __( 'Tagline' ),
5170 'section' => 'title_tagline',
5171 )
5172 );
5173
5174 // Add a setting to hide header text if the theme doesn't support custom headers.
5175 if ( ! current_theme_supports( 'custom-header', 'header-text' ) ) {
5176 $this->add_setting(
5177 'header_text',
5178 array(
5179 'theme_supports' => array( 'custom-logo', 'header-text' ),
5180 'default' => 1,
5181 'sanitize_callback' => 'absint',
5182 )
5183 );
5184
5185 $this->add_control(
5186 'header_text',
5187 array(
5188 'label' => __( 'Display Site Title and Tagline' ),
5189 'section' => 'title_tagline',
5190 'settings' => 'header_text',
5191 'type' => 'checkbox',
5192 )
5193 );
5194 }
5195
5196 $this->add_setting(
5197 'site_icon',
5198 array(
5199 'type' => 'option',
5200 'capability' => 'manage_options',
5201 'transport' => 'postMessage', // Previewed with JS in the Customizer controls window.
5202 )
5203 );
5204
5205 $this->add_control(
5206 new WP_Customize_Site_Icon_Control(
5207 $this,
5208 'site_icon',
5209 array(
5210 'label' => __( 'Site Icon' ),
5211 'description' => sprintf(
5212 /* translators: 1: pixel value for icon size. 2: pixel value for icon size. */
5213 '<p>' . __( 'The Site Icon is what you see in browser tabs, bookmark bars, and within the WordPress mobile apps. It should be square and at least <code>%1$s by %2$s</code> pixels.' ) . '</p>',
5214 512,
5215 512
5216 ),
5217 'section' => 'title_tagline',
5218 'priority' => 60,
5219 'height' => 512,
5220 'width' => 512,
5221 )
5222 )
5223 );
5224
5225 $this->add_setting(
5226 'custom_logo',
5227 array(
5228 'theme_supports' => array( 'custom-logo' ),
5229 'transport' => 'postMessage',
5230 )
5231 );
5232
5233 $custom_logo_args = get_theme_support( 'custom-logo' );
5234 $this->add_control(
5235 new WP_Customize_Cropped_Image_Control(
5236 $this,
5237 'custom_logo',
5238 array(
5239 'label' => __( 'Logo' ),
5240 'section' => 'title_tagline',
5241 'priority' => 8,
5242 'height' => isset( $custom_logo_args[0]['height'] ) ? $custom_logo_args[0]['height'] : null,
5243 'width' => isset( $custom_logo_args[0]['width'] ) ? $custom_logo_args[0]['width'] : null,
5244 'flex_height' => isset( $custom_logo_args[0]['flex-height'] ) ? $custom_logo_args[0]['flex-height'] : null,
5245 'flex_width' => isset( $custom_logo_args[0]['flex-width'] ) ? $custom_logo_args[0]['flex-width'] : null,
5246 'button_labels' => array(
5247 'select' => __( 'Select logo' ),
5248 'change' => __( 'Change logo' ),
5249 'remove' => __( 'Remove' ),
5250 'default' => __( 'Default' ),
5251 'placeholder' => __( 'No logo selected' ),
5252 'frame_title' => __( 'Select logo' ),
5253 'frame_button' => __( 'Choose logo' ),
5254 ),
5255 )
5256 )
5257 );
5258
5259 $this->selective_refresh->add_partial(
5260 'custom_logo',
5261 array(
5262 'settings' => array( 'custom_logo' ),
5263 'selector' => '.custom-logo-link',
5264 'render_callback' => array( $this, '_render_custom_logo_partial' ),
5265 'container_inclusive' => true,
5266 )
5267 );
5268
5269 /* Colors */
5270
5271 $this->add_section(
5272 'colors',
5273 array(
5274 'title' => __( 'Colors' ),
5275 'priority' => 40,
5276 )
5277 );
5278
5279 $this->add_setting(
5280 'header_textcolor',
5281 array(
5282 'theme_supports' => array( 'custom-header', 'header-text' ),
5283 'default' => get_theme_support( 'custom-header', 'default-text-color' ),
5284
5285 'sanitize_callback' => array( $this, '_sanitize_header_textcolor' ),
5286 'sanitize_js_callback' => 'maybe_hash_hex_color',
5287 )
5288 );
5289
5290 // Input type: checkbox, with custom value.
5291 $this->add_control(
5292 'display_header_text',
5293 array(
5294 'settings' => 'header_textcolor',
5295 'label' => __( 'Display Site Title and Tagline' ),
5296 'section' => 'title_tagline',
5297 'type' => 'checkbox',
5298 'priority' => 40,
5299 )
5300 );
5301
5302 $this->add_control(
5303 new WP_Customize_Color_Control(
5304 $this,
5305 'header_textcolor',
5306 array(
5307 'label' => __( 'Header Text Color' ),
5308 'section' => 'colors',
5309 )
5310 )
5311 );
5312
5313 // Input type: color, with sanitize_callback.
5314 $this->add_setting(
5315 'background_color',
5316 array(
5317 'default' => get_theme_support( 'custom-background', 'default-color' ),
5318 'theme_supports' => 'custom-background',
5319
5320 'sanitize_callback' => 'sanitize_hex_color_no_hash',
5321 'sanitize_js_callback' => 'maybe_hash_hex_color',
5322 )
5323 );
5324
5325 $this->add_control(
5326 new WP_Customize_Color_Control(
5327 $this,
5328 'background_color',
5329 array(
5330 'label' => __( 'Background Color' ),
5331 'section' => 'colors',
5332 )
5333 )
5334 );
5335
5336 /* Custom Header */
5337
5338 if ( current_theme_supports( 'custom-header', 'video' ) ) {
5339 $title = __( 'Header Media' );
5340 $description = '<p>' . __( 'If you add a video, the image will be used as a fallback while the video loads.' ) . '</p>';
5341
5342 $width = absint( get_theme_support( 'custom-header', 'width' ) );
5343 $height = absint( get_theme_support( 'custom-header', 'height' ) );
5344 if ( $width && $height ) {
5345 $control_description = sprintf(
5346 /* translators: 1: .mp4, 2: Header size in pixels. */
5347 __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends dimensions of %2$s pixels.' ),
5348 '<code>.mp4</code>',
5349 sprintf( '<strong>%s &times; %s</strong>', $width, $height )
5350 );
5351 } elseif ( $width ) {
5352 $control_description = sprintf(
5353 /* translators: 1: .mp4, 2: Header width in pixels. */
5354 __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a width of %2$s pixels.' ),
5355 '<code>.mp4</code>',
5356 sprintf( '<strong>%s</strong>', $width )
5357 );
5358 } else {
5359 $control_description = sprintf(
5360 /* translators: 1: .mp4, 2: Header height in pixels. */
5361 __( 'Upload your video in %1$s format and minimize its file size for best results. Your theme recommends a height of %2$s pixels.' ),
5362 '<code>.mp4</code>',
5363 sprintf( '<strong>%s</strong>', $height )
5364 );
5365 }
5366 } else {
5367 $title = __( 'Header Image' );
5368 $description = '';
5369 $control_description = '';
5370 }
5371
5372 $this->add_section(
5373 'header_image',
5374 array(
5375 'title' => $title,
5376 'description' => $description,
5377 'theme_supports' => 'custom-header',
5378 'priority' => 60,
5379 )
5380 );
5381
5382 $this->add_setting(
5383 'header_video',
5384 array(
5385 'theme_supports' => array( 'custom-header', 'video' ),
5386 'transport' => 'postMessage',
5387 'sanitize_callback' => 'absint',
5388 'validate_callback' => array( $this, '_validate_header_video' ),
5389 )
5390 );
5391
5392 $this->add_setting(
5393 'external_header_video',
5394 array(
5395 'theme_supports' => array( 'custom-header', 'video' ),
5396 'transport' => 'postMessage',
5397 'sanitize_callback' => array( $this, '_sanitize_external_header_video' ),
5398 'validate_callback' => array( $this, '_validate_external_header_video' ),
5399 )
5400 );
5401
5402 $this->add_setting(
5403 new WP_Customize_Filter_Setting(
5404 $this,
5405 'header_image',
5406 array(
5407 'default' => sprintf( get_theme_support( 'custom-header', 'default-image' ), get_template_directory_uri(), get_stylesheet_directory_uri() ),
5408 'theme_supports' => 'custom-header',
5409 )
5410 )
5411 );
5412
5413 $this->add_setting(
5414 new WP_Customize_Header_Image_Setting(
5415 $this,
5416 'header_image_data',
5417 array(
5418 'theme_supports' => 'custom-header',
5419 )
5420 )
5421 );
5422
5423 /*
5424 * Switch image settings to postMessage when video support is enabled since
5425 * it entails that the_custom_header_markup() will be used, and thus selective
5426 * refresh can be utilized.
5427 */
5428 if ( current_theme_supports( 'custom-header', 'video' ) ) {
5429 $this->get_setting( 'header_image' )->transport = 'postMessage';
5430 $this->get_setting( 'header_image_data' )->transport = 'postMessage';
5431 }
5432
5433 $this->add_control(
5434 new WP_Customize_Media_Control(
5435 $this,
5436 'header_video',
5437 array(
5438 'theme_supports' => array( 'custom-header', 'video' ),
5439 'label' => __( 'Header Video' ),
5440 'description' => $control_description,
5441 'section' => 'header_image',
5442 'mime_type' => 'video',
5443 'active_callback' => 'is_header_video_active',
5444 )
5445 )
5446 );
5447
5448 $this->add_control(
5449 'external_header_video',
5450 array(
5451 'theme_supports' => array( 'custom-header', 'video' ),
5452 'type' => 'url',
5453 'description' => __( 'Or, enter a YouTube URL:' ),
5454 'section' => 'header_image',
5455 'active_callback' => 'is_header_video_active',
5456 )
5457 );
5458
5459 $this->add_control( new WP_Customize_Header_Image_Control( $this ) );
5460
5461 $this->selective_refresh->add_partial(
5462 'custom_header',
5463 array(
5464 'selector' => '#wp-custom-header',
5465 'render_callback' => 'the_custom_header_markup',
5466 'settings' => array( 'header_video', 'external_header_video', 'header_image' ), // The image is used as a video fallback here.
5467 'container_inclusive' => true,
5468 )
5469 );
5470
5471 /* Custom Background */
5472
5473 $this->add_section(
5474 'background_image',
5475 array(
5476 'title' => __( 'Background Image' ),
5477 'theme_supports' => 'custom-background',
5478 'priority' => 80,
5479 )
5480 );
5481
5482 $this->add_setting(
5483 'background_image',
5484 array(
5485 'default' => get_theme_support( 'custom-background', 'default-image' ),
5486 'theme_supports' => 'custom-background',
5487 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
5488 )
5489 );
5490
5491 $this->add_setting(
5492 new WP_Customize_Background_Image_Setting(
5493 $this,
5494 'background_image_thumb',
5495 array(
5496 'theme_supports' => 'custom-background',
5497 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
5498 )
5499 )
5500 );
5501
5502 $this->add_control( new WP_Customize_Background_Image_Control( $this ) );
5503
5504 $this->add_setting(
5505 'background_preset',
5506 array(
5507 'default' => get_theme_support( 'custom-background', 'default-preset' ),
5508 'theme_supports' => 'custom-background',
5509 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
5510 )
5511 );
5512
5513 $this->add_control(
5514 'background_preset',
5515 array(
5516 'label' => _x( 'Preset', 'Background Preset' ),
5517 'section' => 'background_image',
5518 'type' => 'select',
5519 'choices' => array(
5520 'default' => _x( 'Default', 'Default Preset' ),
5521 'fill' => __( 'Fill Screen' ),
5522 'fit' => __( 'Fit to Screen' ),
5523 'repeat' => _x( 'Repeat', 'Repeat Image' ),
5524 'custom' => _x( 'Custom', 'Custom Preset' ),
5525 ),
5526 )
5527 );
5528
5529 $this->add_setting(
5530 'background_position_x',
5531 array(
5532 'default' => get_theme_support( 'custom-background', 'default-position-x' ),
5533 'theme_supports' => 'custom-background',
5534 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
5535 )
5536 );
5537
5538 $this->add_setting(
5539 'background_position_y',
5540 array(
5541 'default' => get_theme_support( 'custom-background', 'default-position-y' ),
5542 'theme_supports' => 'custom-background',
5543 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
5544 )
5545 );
5546
5547 $this->add_control(
5548 new WP_Customize_Background_Position_Control(
5549 $this,
5550 'background_position',
5551 array(
5552 'label' => __( 'Image Position' ),
5553 'section' => 'background_image',
5554 'settings' => array(
5555 'x' => 'background_position_x',
5556 'y' => 'background_position_y',
5557 ),
5558 )
5559 )
5560 );
5561
5562 $this->add_setting(
5563 'background_size',
5564 array(
5565 'default' => get_theme_support( 'custom-background', 'default-size' ),
5566 'theme_supports' => 'custom-background',
5567 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
5568 )
5569 );
5570
5571 $this->add_control(
5572 'background_size',
5573 array(
5574 'label' => __( 'Image Size' ),
5575 'section' => 'background_image',
5576 'type' => 'select',
5577 'choices' => array(
5578 'auto' => _x( 'Original', 'Original Size' ),
5579 'contain' => __( 'Fit to Screen' ),
5580 'cover' => __( 'Fill Screen' ),
5581 ),
5582 )
5583 );
5584
5585 $this->add_setting(
5586 'background_repeat',
5587 array(
5588 'default' => get_theme_support( 'custom-background', 'default-repeat' ),
5589 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
5590 'theme_supports' => 'custom-background',
5591 )
5592 );
5593
5594 $this->add_control(
5595 'background_repeat',
5596 array(
5597 'label' => __( 'Repeat Background Image' ),
5598 'section' => 'background_image',
5599 'type' => 'checkbox',
5600 )
5601 );
5602
5603 $this->add_setting(
5604 'background_attachment',
5605 array(
5606 'default' => get_theme_support( 'custom-background', 'default-attachment' ),
5607 'sanitize_callback' => array( $this, '_sanitize_background_setting' ),
5608 'theme_supports' => 'custom-background',
5609 )
5610 );
5611
5612 $this->add_control(
5613 'background_attachment',
5614 array(
5615 'label' => __( 'Scroll with Page' ),
5616 'section' => 'background_image',
5617 'type' => 'checkbox',
5618 )
5619 );
5620
5621 /*
5622 * If the theme is using the default background callback, we can update
5623 * the background CSS using postMessage.
5624 */
5625 if ( get_theme_support( 'custom-background', 'wp-head-callback' ) === '_custom_background_cb' ) {
5626 foreach ( array( 'color', 'image', 'preset', 'position_x', 'position_y', 'size', 'repeat', 'attachment' ) as $prop ) {
5627 $this->get_setting( 'background_' . $prop )->transport = 'postMessage';
5628 }
5629 }
5630
5631 /*
5632 * Static Front Page
5633 * See also https://core.trac.wordpress.org/ticket/19627 which introduces the static-front-page theme_support.
5634 * The following replicates behavior from options-reading.php.
5635 */
5636
5637 $this->add_section(
5638 'static_front_page',
5639 array(
5640 'title' => __( 'Homepage Settings' ),
5641 'priority' => 120,
5642 'description' => __( 'You can choose what&#8217;s displayed on the homepage of your site. It can be posts in reverse chronological order (classic blog), or a fixed/static page. To set a static homepage, you first need to create two Pages. One will become the homepage, and the other will be where your posts are displayed.' ),
5643 'active_callback' => array( $this, 'has_published_pages' ),
5644 )
5645 );
5646
5647 $this->add_setting(
5648 'show_on_front',
5649 array(
5650 'default' => get_option( 'show_on_front' ),
5651 'capability' => 'manage_options',
5652 'type' => 'option',
5653 )
5654 );
5655
5656 $this->add_control(
5657 'show_on_front',
5658 array(
5659 'label' => __( 'Your homepage displays' ),
5660 'section' => 'static_front_page',
5661 'type' => 'radio',
5662 'choices' => array(
5663 'posts' => __( 'Your latest posts' ),
5664 'page' => __( 'A static page' ),
5665 ),
5666 )
5667 );
5668
5669 $this->add_setting(
5670 'page_on_front',
5671 array(
5672 'type' => 'option',
5673 'capability' => 'manage_options',
5674 )
5675 );
5676
5677 $this->add_control(
5678 'page_on_front',
5679 array(
5680 'label' => __( 'Homepage' ),
5681 'section' => 'static_front_page',
5682 'type' => 'dropdown-pages',
5683 'allow_addition' => true,
5684 )
5685 );
5686
5687 $this->add_setting(
5688 'page_for_posts',
5689 array(
5690 'type' => 'option',
5691 'capability' => 'manage_options',
5692 )
5693 );
5694
5695 $this->add_control(
5696 'page_for_posts',
5697 array(
5698 'label' => __( 'Posts page' ),
5699 'section' => 'static_front_page',
5700 'type' => 'dropdown-pages',
5701 'allow_addition' => true,
5702 )
5703 );
5704
5705 /* Custom CSS */
5706 $section_description = '<p>';
5707 $section_description .= __( 'Add your own CSS code here to customize the appearance and layout of your site.' );
5708 $section_description .= sprintf(
5709 ' <a href="%1$s" class="external-link" target="_blank">%2$s<span class="screen-reader-text"> %3$s</span></a>',
5710 esc_url( __( 'https://developer.wordpress.org/advanced-administration/wordpress/css/' ) ),
5711 __( 'Learn more about CSS' ),
5712 /* translators: Hidden accessibility text. */
5713 __( '(opens in a new tab)' )
5714 );
5715 $section_description .= '</p>';
5716
5717 $section_description .= '<p id="editor-keyboard-trap-help-1">' . __( 'When using a keyboard to navigate:' ) . '</p>';
5718 $section_description .= '<ul>';
5719 $section_description .= '<li id="editor-keyboard-trap-help-2">' . __( 'In the editing area, the Tab key enters a tab character.' ) . '</li>';
5720 $section_description .= '<li id="editor-keyboard-trap-help-3">' . __( 'To move away from this area, press the Esc key followed by the Tab key.' ) . '</li>';
5721 $section_description .= '<li id="editor-keyboard-trap-help-4">' . __( 'Screen reader users: when in forms mode, you may need to press the Esc key twice.' ) . '</li>';
5722 $section_description .= '</ul>';
5723
5724 if ( 'false' !== wp_get_current_user()->syntax_highlighting ) {
5725 $section_description .= '<p>';
5726 $section_description .= sprintf(
5727 /* translators: 1: Link to user profile, 2: Additional link attributes, 3: Accessibility text. */
5728 __( 'The edit field automatically highlights code syntax. You can disable this in your <a href="%1$s" %2$s>user profile%3$s</a> to work in plain text mode.' ),
5729 esc_url( get_edit_profile_url() ),
5730 'class="external-link" target="_blank"',
5731 sprintf(
5732 '<span class="screen-reader-text"> %s</span>',
5733 /* translators: Hidden accessibility text. */
5734 __( '(opens in a new tab)' )
5735 )
5736 );
5737 $section_description .= '</p>';
5738 }
5739
5740 $section_description .= '<p class="section-description-buttons">';
5741 $section_description .= '<button type="button" class="button-link section-description-close">' . __( 'Close' ) . '</button>';
5742 $section_description .= '</p>';
5743
5744 $this->add_section(
5745 'custom_css',
5746 array(
5747 'title' => __( 'Additional CSS' ),
5748 'priority' => 200,
5749 'description_hidden' => true,
5750 'description' => $section_description,
5751 )
5752 );
5753
5754 $custom_css_setting = new WP_Customize_Custom_CSS_Setting(
5755 $this,
5756 sprintf( 'custom_css[%s]', get_stylesheet() ),
5757 array(
5758 'capability' => 'edit_css',
5759 'default' => '',
5760 )
5761 );
5762 $this->add_setting( $custom_css_setting );
5763
5764 $this->add_control(
5765 new WP_Customize_Code_Editor_Control(
5766 $this,
5767 'custom_css',
5768 array(
5769 'label' => __( 'CSS code' ),
5770 'section' => 'custom_css',
5771 'settings' => array( 'default' => $custom_css_setting->id ),
5772 'code_type' => 'text/css',
5773 'input_attrs' => array(
5774 'aria-describedby' => 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4',
5775 ),
5776 )
5777 )
5778 );
5779 }
5780
5781 /**
5782 * Returns whether there are published pages.
5783 *
5784 * Used as active callback for static front page section and controls.
5785 *
5786 * @since 4.7.0
5787 *
5788 * @return bool Whether there are published (or to be published) pages.
5789 */
5790 public function has_published_pages() {
5791
5792 $setting = $this->get_setting( 'nav_menus_created_posts' );
5793 if ( $setting ) {
5794 foreach ( $setting->value() as $post_id ) {
5795 if ( 'page' === get_post_type( $post_id ) ) {
5796 return true;
5797 }
5798 }
5799 }
5800
5801 return 0 !== count(
5802 get_pages(
5803 array(
5804 'number' => 1,
5805 'hierarchical' => 0,
5806 )
5807 )
5808 );
5809 }
5810
5811 /**
5812 * Adds settings from the POST data that were not added with code, e.g. dynamically-created settings for Widgets
5813 *
5814 * @since 4.2.0
5815 *
5816 * @see add_dynamic_settings()
5817 */
5818 public function register_dynamic_settings() {
5819 $setting_ids = array_keys( $this->unsanitized_post_values() );
5820 $this->add_dynamic_settings( $setting_ids );
5821 }
5822
5823 /**
5824 * Loads themes into the theme browsing/installation UI.
5825 *
5826 * @since 4.9.0
5827 */
5828 public function handle_load_themes_request() {
5829 check_ajax_referer( 'switch_themes', 'nonce' );
5830
5831 if ( ! current_user_can( 'switch_themes' ) ) {
5832 wp_die( -1 );
5833 }
5834
5835 if ( empty( $_POST['theme_action'] ) ) {
5836 wp_send_json_error( 'missing_theme_action' );
5837 }
5838 $theme_action = sanitize_key( $_POST['theme_action'] );
5839 $themes = array();
5840 $args = array();
5841
5842 // Define query filters based on user input.
5843 if ( ! array_key_exists( 'search', $_POST ) ) {
5844 $args['search'] = '';
5845 } else {
5846 $args['search'] = sanitize_text_field( wp_unslash( $_POST['search'] ) );
5847 }
5848
5849 if ( ! array_key_exists( 'tags', $_POST ) ) {
5850 $args['tag'] = '';
5851 } else {
5852 $args['tag'] = array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['tags'] ) );
5853 }
5854
5855 if ( ! array_key_exists( 'page', $_POST ) ) {
5856 $args['page'] = 1;
5857 } else {
5858 $args['page'] = absint( $_POST['page'] );
5859 }
5860
5861 require_once ABSPATH . 'wp-admin/includes/theme.php';
5862
5863 if ( 'installed' === $theme_action ) {
5864
5865 // Load all installed themes from wp_prepare_themes_for_js().
5866 $themes = array( 'themes' => array() );
5867 foreach ( wp_prepare_themes_for_js() as $theme ) {
5868 $theme['type'] = 'installed';
5869 $theme['active'] = ( isset( $_POST['customized_theme'] ) && $_POST['customized_theme'] === $theme['id'] );
5870 $themes['themes'][] = $theme;
5871 }
5872 } elseif ( 'wporg' === $theme_action ) {
5873
5874 // Load WordPress.org themes from the .org API and normalize data to match installed theme objects.
5875 if ( ! current_user_can( 'install_themes' ) ) {
5876 wp_die( -1 );
5877 }
5878
5879 // Arguments for all queries.
5880 $wporg_args = array(
5881 'per_page' => 100,
5882 'fields' => array(
5883 'reviews_url' => true, // Explicitly request the reviews URL to be linked from the customizer.
5884 ),
5885 );
5886
5887 $args = array_merge( $wporg_args, $args );
5888
5889 if ( '' === $args['search'] && '' === $args['tag'] ) {
5890 $args['browse'] = 'new'; // Sort by latest themes by default.
5891 }
5892
5893 // Load themes from the .org API.
5894 $themes = themes_api( 'query_themes', $args );
5895 if ( is_wp_error( $themes ) ) {
5896 wp_send_json_error();
5897 }
5898
5899 // This list matches the allowed tags in wp-admin/includes/theme-install.php.
5900 $themes_allowedtags = array_fill_keys(
5901 array( 'a', 'abbr', 'acronym', 'code', 'pre', 'em', 'strong', 'div', 'p', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img' ),
5902 array()
5903 );
5904 $themes_allowedtags['a'] = array_fill_keys( array( 'href', 'title', 'target' ), true );
5905 $themes_allowedtags['acronym']['title'] = true;
5906 $themes_allowedtags['abbr']['title'] = true;
5907 $themes_allowedtags['img'] = array_fill_keys( array( 'src', 'class', 'alt' ), true );
5908
5909 // Prepare a list of installed themes to check against before the loop.
5910 $installed_themes = array();
5911 $wp_themes = wp_get_themes();
5912 foreach ( $wp_themes as $theme ) {
5913 $installed_themes[] = $theme->get_stylesheet();
5914 }
5915 $update_php = network_admin_url( 'update.php?action=install-theme' );
5916
5917 // Set up properties for themes available on WordPress.org.
5918 foreach ( $themes->themes as &$theme ) {
5919 $theme->install_url = add_query_arg(
5920 array(
5921 'theme' => $theme->slug,
5922 '_wpnonce' => wp_create_nonce( 'install-theme_' . $theme->slug ),
5923 ),
5924 $update_php
5925 );
5926
5927 $theme->name = wp_kses( $theme->name, $themes_allowedtags );
5928 $theme->version = wp_kses( $theme->version, $themes_allowedtags );
5929 $theme->description = wp_kses( $theme->description, $themes_allowedtags );
5930 $theme->stars = wp_star_rating(
5931 array(
5932 'rating' => $theme->rating,
5933 'type' => 'percent',
5934 'number' => $theme->num_ratings,
5935 'echo' => false,
5936 )
5937 );
5938 $theme->num_ratings = number_format_i18n( $theme->num_ratings );
5939 $theme->preview_url = set_url_scheme( $theme->preview_url );
5940
5941 // Handle themes that are already installed as installed themes.
5942 if ( in_array( $theme->slug, $installed_themes, true ) ) {
5943 $theme->type = 'installed';
5944 } else {
5945 $theme->type = $theme_action;
5946 }
5947
5948 // Set active based on customized theme.
5949 $theme->active = ( isset( $_POST['customized_theme'] ) && $_POST['customized_theme'] === $theme->slug );
5950
5951 // Map available theme properties to installed theme properties.
5952 $theme->id = $theme->slug;
5953 $theme->screenshot = array( $theme->screenshot_url );
5954 $theme->authorAndUri = wp_kses( $theme->author['display_name'], $themes_allowedtags );
5955 $theme->compatibleWP = is_wp_version_compatible( $theme->requires ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName
5956 $theme->compatiblePHP = is_php_version_compatible( $theme->requires_php ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName
5957
5958 if ( isset( $theme->parent ) ) {
5959 $theme->parent = $theme->parent['slug'];
5960 } else {
5961 $theme->parent = false;
5962 }
5963 unset( $theme->slug );
5964 unset( $theme->screenshot_url );
5965 unset( $theme->author );
5966 } // End foreach().
5967 } // End if().
5968
5969 /**
5970 * Filters the theme data loaded in the customizer.
5971 *
5972 * This allows theme data to be loading from an external source,
5973 * or modification of data loaded from `wp_prepare_themes_for_js()`
5974 * or WordPress.org via `themes_api()`.
5975 *
5976 * @since 4.9.0
5977 *
5978 * @see wp_prepare_themes_for_js()
5979 * @see themes_api()
5980 * @see WP_Customize_Manager::__construct()
5981 *
5982 * @param array|stdClass $themes Nested array or object of theme data.
5983 * @param array $args List of arguments, such as page, search term, and tags to query for.
5984 * @param WP_Customize_Manager $manager Instance of Customize manager.
5985 */
5986 $themes = apply_filters( 'customize_load_themes', $themes, $args, $this );
5987
5988 wp_send_json_success( $themes );
5989 }
5990
5991
5992 /**
5993 * Callback for validating the header_textcolor value.
5994 *
5995 * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
5996 * Returns default text color if hex color is empty.
5997 *
5998 * @since 3.4.0
5999 *
6000 * @param string $color
6001 * @return mixed
6002 */
6003 public function _sanitize_header_textcolor( $color ) {
6004 if ( 'blank' === $color ) {
6005 return 'blank';
6006 }
6007
6008 $color = sanitize_hex_color_no_hash( $color );
6009 if ( empty( $color ) ) {
6010 $color = get_theme_support( 'custom-header', 'default-text-color' );
6011 }
6012
6013 return $color;
6014 }
6015
6016 /**
6017 * Callback for validating a background setting value.
6018 *
6019 * @since 4.7.0
6020 *
6021 * @param string $value Repeat value.
6022 * @param WP_Customize_Setting $setting Setting.
6023 * @return string|WP_Error Background value or validation error.
6024 */
6025 public function _sanitize_background_setting( $value, $setting ) {
6026 if ( 'background_repeat' === $setting->id ) {
6027 if ( ! in_array( $value, array( 'repeat-x', 'repeat-y', 'repeat', 'no-repeat' ), true ) ) {
6028 return new WP_Error( 'invalid_value', __( 'Invalid value for background repeat.' ) );
6029 }
6030 } elseif ( 'background_attachment' === $setting->id ) {
6031 if ( ! in_array( $value, array( 'fixed', 'scroll' ), true ) ) {
6032 return new WP_Error( 'invalid_value', __( 'Invalid value for background attachment.' ) );
6033 }
6034 } elseif ( 'background_position_x' === $setting->id ) {
6035 if ( ! in_array( $value, array( 'left', 'center', 'right' ), true ) ) {
6036 return new WP_Error( 'invalid_value', __( 'Invalid value for background position X.' ) );
6037 }
6038 } elseif ( 'background_position_y' === $setting->id ) {
6039 if ( ! in_array( $value, array( 'top', 'center', 'bottom' ), true ) ) {
6040 return new WP_Error( 'invalid_value', __( 'Invalid value for background position Y.' ) );
6041 }
6042 } elseif ( 'background_size' === $setting->id ) {
6043 if ( ! in_array( $value, array( 'auto', 'contain', 'cover' ), true ) ) {
6044 return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
6045 }
6046 } elseif ( 'background_preset' === $setting->id ) {
6047 if ( ! in_array( $value, array( 'default', 'fill', 'fit', 'repeat', 'custom' ), true ) ) {
6048 return new WP_Error( 'invalid_value', __( 'Invalid value for background size.' ) );
6049 }
6050 } elseif ( 'background_image' === $setting->id || 'background_image_thumb' === $setting->id ) {
6051 $value = empty( $value ) ? '' : sanitize_url( $value );
6052 } else {
6053 return new WP_Error( 'unrecognized_setting', __( 'Unrecognized background setting.' ) );
6054 }
6055 return $value;
6056 }
6057
6058 /**
6059 * Exports header video settings to facilitate selective refresh.
6060 *
6061 * @since 4.7.0
6062 *
6063 * @param array $response Response.
6064 * @param WP_Customize_Selective_Refresh $selective_refresh Selective refresh component.
6065 * @param array $partials Array of partials.
6066 * @return array
6067 */
6068 public function export_header_video_settings( $response, $selective_refresh, $partials ) {
6069 if ( isset( $partials['custom_header'] ) ) {
6070 $response['custom_header_settings'] = get_header_video_settings();
6071 }
6072
6073 return $response;
6074 }
6075
6076 /**
6077 * Callback for validating the header_video value.
6078 *
6079 * Ensures that the selected video is less than 8MB and provides an error message.
6080 *
6081 * @since 4.7.0
6082 *
6083 * @param WP_Error $validity
6084 * @param mixed $value
6085 * @return mixed
6086 */
6087 public function _validate_header_video( $validity, $value ) {
6088 $video = get_attached_file( absint( $value ) );
6089 if ( $video ) {
6090 $size = filesize( $video );
6091 if ( $size > 8 * MB_IN_BYTES ) {
6092 $validity->add(
6093 'size_too_large',
6094 __( '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.' )
6095 );
6096 }
6097 if ( ! str_ends_with( $video, '.mp4' ) && ! str_ends_with( $video, '.mov' ) ) { // Check for .mp4 or .mov format, which (assuming h.264 encoding) are the only cross-browser-supported formats.
6098 $validity->add(
6099 'invalid_file_type',
6100 sprintf(
6101 /* translators: 1: .mp4, 2: .mov */
6102 __( '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.' ),
6103 '<code>.mp4</code>',
6104 '<code>.mov</code>'
6105 )
6106 );
6107 }
6108 }
6109 return $validity;
6110 }
6111
6112 /**
6113 * Callback for validating the external_header_video value.
6114 *
6115 * Ensures that the provided URL is supported.
6116 *
6117 * @since 4.7.0
6118 *
6119 * @param WP_Error $validity
6120 * @param mixed $value
6121 * @return mixed
6122 */
6123 public function _validate_external_header_video( $validity, $value ) {
6124 $video = sanitize_url( $value );
6125 if ( $video ) {
6126 if ( ! preg_match( '#^https?://(?:www\.)?(?:youtube\.com/watch|youtu\.be/)#', $video ) ) {
6127 $validity->add( 'invalid_url', __( 'Please enter a valid YouTube URL.' ) );
6128 }
6129 }
6130 return $validity;
6131 }
6132
6133 /**
6134 * Callback for sanitizing the external_header_video value.
6135 *
6136 * @since 4.7.1
6137 *
6138 * @param string $value URL.
6139 * @return string Sanitized URL.
6140 */
6141 public function _sanitize_external_header_video( $value ) {
6142 return sanitize_url( trim( $value ) );
6143 }
6144
6145 /**
6146 * Callback for rendering the custom logo, used in the custom_logo partial.
6147 *
6148 * This method exists because the partial object and context data are passed
6149 * into a partial's render_callback so we cannot use get_custom_logo() as
6150 * the render_callback directly since it expects a blog ID as the first
6151 * argument.
6152 *
6153 * @see WP_Customize_Manager::register_controls()
6154 *
6155 * @since 4.5.0
6156 *
6157 * @return string Custom logo.
6158 */
6159 public function _render_custom_logo_partial() {
6160 return get_custom_logo();
6161 }
6162}
6163
Ui Ux Design – Teachers Night Out https://cardgames4educators.com Wed, 16 Oct 2024 22:24:18 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.4 https://cardgames4educators.com/wp-content/uploads/2024/06/cropped-Card-4-Educators-logo-32x32.png Ui Ux Design – Teachers Night Out https://cardgames4educators.com 32 32 Masters In English How English Speaker https://cardgames4educators.com/masters-in-english-how-english-speaker/ https://cardgames4educators.com/masters-in-english-how-english-speaker/#comments Mon, 27 May 2024 08:54:45 +0000 https://themexriver.com/wp/kadu/?p=1

Erat himenaeos neque id sagittis massa. Hac suscipit pulvinar dignissim platea magnis eu. Don tellus a pharetra inceptos efficitur dui pulvinar. Feugiat facilisis penatibus pulvinar nunc dictumst donec odio platea habitasse. Lacus porta dolor purus elit ante bibendum tortor netus taciti nullam cubilia. Erat per suspendisse placerat morbi egestas pulvinar bibendum sollicitudin nec. Euismod cubilia eleifend velit himenaeos sodales lectus. Leo maximus cras ac porttitor aliquam torquent pulvinar odio volutpat parturient. Quisque risus finibus suspendisse mus purus magnis facilisi condimentum consectetur dui. Curae elit suspendisse cursus vehicula.

Turpis taciti class non vel pretium quis pulvinar tempor lobortis nunc. Libero phasellus parturient sapien volutpat malesuada ornare. Cubilia dignissim sollicitudin rhoncus lacinia maximus. Cras lorem fermentum bibendum pellentesque nisl etiam ligula enim cubilia. Vulputate pede sapien torquent montes tempus malesuada in mattis dis turpis vitae. Porta est tempor ex eget feugiat vulputate ipsum. Justo nec iaculis habitant diam arcu fermentum.

We offer comprehen sive emplo ment services such as assistance wit employer compliance.Our company is your strategic HR partner as instead of HR. john smithson

Cubilia dignissim sollicitudin rhoncus lacinia maximus. Cras lorem fermentum bibendum pellentesque nisl etiam ligula enim cubilia. Vulputate pede sapien torquent montes tempus malesuada in mattis dis turpis vitae.

Exploring Learning Landscapes in Academic

Feugiat facilisis penatibus pulvinar nunc dictumst donec odio platea habitasse. Lacus porta dolor purus elit ante bibendum tortor netus taciti nullam cubilia. Erat per suspendisse placerat morbi egestas pulvinar bibendum sollicitudin nec. Euismod cubilia eleifend velit himenaeos sodales lectus. Leo maximus cras ac porttitor aliquam torquent.

]]>
https://cardgames4educators.com/masters-in-english-how-english-speaker/feed/ 1