WordPress 4.7
[autoinstalls/wordpress.git] / wp-includes / customize / class-wp-customize-custom-css-setting.php
1 <?php
2 /**
3  * Customize API: WP_Customize_Custom_CSS_Setting class
4  *
5  * This handles validation, sanitization and saving of the value.
6  *
7  * @package WordPress
8  * @subpackage Customize
9  * @since 4.7.0
10  */
11
12 /**
13  * Custom Setting to handle WP Custom CSS.
14  *
15  * @since 4.7.0
16  *
17  * @see WP_Customize_Setting
18  */
19 final class WP_Customize_Custom_CSS_Setting extends WP_Customize_Setting {
20
21         /**
22          * The setting type.
23          *
24          * @since 4.7.0
25          * @access public
26          * @var string
27          */
28         public $type = 'custom_css';
29
30         /**
31          * Setting Transport
32          *
33          * @since 4.7.0
34          * @access public
35          * @var string
36          */
37         public $transport = 'postMessage';
38
39         /**
40          * Capability required to edit this setting.
41          *
42          * @since 4.7.0
43          * @access public
44          * @var string
45          */
46         public $capability = 'edit_css';
47
48         /**
49          * Stylesheet
50          *
51          * @since 4.7.0
52          * @access public
53          * @var string
54          */
55         public $stylesheet = '';
56
57         /**
58          * WP_Customize_Custom_CSS_Setting constructor.
59          *
60          * @since 4.7.0
61          * @access public
62          *
63          * @throws Exception If the setting ID does not match the pattern `custom_css[$stylesheet]`.
64          *
65          * @param WP_Customize_Manager $manager The Customize Manager class.
66          * @param string               $id      An specific ID of the setting. Can be a
67          *                                      theme mod or option name.
68          * @param array                $args    Setting arguments.
69          */
70         public function __construct( $manager, $id, $args = array() ) {
71                 parent::__construct( $manager, $id, $args );
72                 if ( 'custom_css' !== $this->id_data['base'] ) {
73                         throw new Exception( 'Expected custom_css id_base.' );
74                 }
75                 if ( 1 !== count( $this->id_data['keys'] ) || empty( $this->id_data['keys'][0] ) ) {
76                         throw new Exception( 'Expected single stylesheet key.' );
77                 }
78                 $this->stylesheet = $this->id_data['keys'][0];
79         }
80
81         /**
82          * Add filter to preview post value.
83          *
84          * @since 4.7.9
85          * @access public
86          *
87          * @return bool False when preview short-circuits due no change needing to be previewed.
88          */
89         public function preview() {
90                 if ( $this->is_previewed ) {
91                         return false;
92                 }
93                 $this->is_previewed = true;
94                 add_filter( 'wp_get_custom_css', array( $this, 'filter_previewed_wp_get_custom_css' ), 9, 2 );
95                 return true;
96         }
97
98         /**
99          * Filter `wp_get_custom_css` for applying the customized value.
100          *
101          * This is used in the preview when `wp_get_custom_css()` is called for rendering the styles.
102          *
103          * @since 4.7.0
104          * @access private
105          * @see wp_get_custom_css()
106          *
107          * @param string $css        Original CSS.
108          * @param string $stylesheet Current stylesheet.
109          * @return string CSS.
110          */
111         public function filter_previewed_wp_get_custom_css( $css, $stylesheet ) {
112                 if ( $stylesheet === $this->stylesheet ) {
113                         $customized_value = $this->post_value( null );
114                         if ( ! is_null( $customized_value ) ) {
115                                 $css = $customized_value;
116                         }
117                 }
118                 return $css;
119         }
120
121         /**
122          * Fetch the value of the setting. Will return the previewed value when `preview()` is called.
123          *
124          * @since 4.7.0
125          * @access public
126          * @see WP_Customize_Setting::value()
127          *
128          * @return string
129          */
130         public function value() {
131                 if ( $this->is_previewed ) {
132                         $post_value = $this->post_value( null );
133                         if ( null !== $post_value ) {
134                                 return $post_value;
135                         }
136                 }
137                 $id_base = $this->id_data['base'];
138                 $value = '';
139                 $post = wp_get_custom_css_post( $this->stylesheet );
140                 if ( $post ) {
141                         $value = $post->post_content;
142                 }
143                 if ( empty( $value ) ) {
144                         $value = $this->default;
145                 }
146
147                 /** This filter is documented in wp-includes/class-wp-customize-setting.php */
148                 $value = apply_filters( "customize_value_{$id_base}", $value, $this );
149
150                 return $value;
151         }
152
153         /**
154          * Validate CSS.
155          *
156          * Checks for imbalanced braces, brackets, and comments.
157          * Notifications are rendered when the customizer state is saved.
158          *
159          * @todo There are cases where valid CSS can be incorrectly marked as invalid when strings or comments include balancing characters. To fix, CSS tokenization needs to be used.
160          *
161          * @since 4.7.0
162          * @access public
163          *
164          * @param string $css The input string.
165          * @return true|WP_Error True if the input was validated, otherwise WP_Error.
166          */
167         public function validate( $css ) {
168                 $validity = new WP_Error();
169
170                 if ( preg_match( '#</?\w+#', $css ) ) {
171                         $validity->add( 'illegal_markup', __( 'Markup is not allowed in CSS.' ) );
172                 }
173
174                 $imbalanced = false;
175
176                 // Make sure that there is a closing brace for each opening brace.
177                 if ( ! $this->validate_balanced_characters( '{', '}', $css ) ) {
178                         $validity->add( 'imbalanced_curly_brackets', __( 'Your curly brackets <code>{}</code> are imbalanced. Make sure there is a closing <code>}</code> for every opening <code>{</code>.' ) );
179                         $imbalanced = true;
180                 }
181
182                 // Ensure brackets are balanced.
183                 if ( ! $this->validate_balanced_characters( '[', ']', $css ) ) {
184                         $validity->add( 'imbalanced_braces', __( 'Your brackets <code>[]</code> are imbalanced. Make sure there is a closing <code>]</code> for every opening <code>[</code>.' ) );
185                         $imbalanced = true;
186                 }
187
188                 // Ensure parentheses are balanced.
189                 if ( ! $this->validate_balanced_characters( '(', ')', $css ) ) {
190                         $validity->add( 'imbalanced_parentheses', __( 'Your parentheses <code>()</code> are imbalanced. Make sure there is a closing <code>)</code> for every opening <code>(</code>.' ) );
191                         $imbalanced = true;
192                 }
193
194                 // Ensure single quotes are equal.
195                 if ( ! $this->validate_equal_characters( '\'', $css ) ) {
196                         $validity->add( 'unequal_single_quotes', __( 'Your single quotes <code>\'</code> are uneven. Make sure there is a closing <code>\'</code> for every opening <code>\'</code>.' ) );
197                         $imbalanced = true;
198                 }
199
200                 // Ensure single quotes are equal.
201                 if ( ! $this->validate_equal_characters( '"', $css ) ) {
202                         $validity->add( 'unequal_double_quotes', __( 'Your double quotes <code>"</code> are uneven. Make sure there is a closing <code>"</code> for every opening <code>"</code>.' ) );
203                         $imbalanced = true;
204                 }
205
206                 /*
207                  * Make sure any code comments are closed properly.
208                  *
209                  * The first check could miss stray an unpaired comment closing figure, so if
210                  * The number appears to be balanced, then check for equal numbers
211                  * of opening/closing comment figures.
212                  *
213                  * Although it may initially appear redundant, we use the first method
214                  * to give more specific feedback to the user.
215                  */
216                 $unclosed_comment_count = $this->validate_count_unclosed_comments( $css );
217                 if ( 0 < $unclosed_comment_count ) {
218                         $validity->add( 'unclosed_comment', sprintf( _n( 'There is %s unclosed code comment. Close each comment with <code>*/</code>.', 'There are %s unclosed code comments. Close each comment with <code>*/</code>.', $unclosed_comment_count ), $unclosed_comment_count ) );
219                         $imbalanced = true;
220                 } elseif ( ! $this->validate_balanced_characters( '/*', '*/', $css ) ) {
221                         $validity->add( 'imbalanced_comments', __( 'There is an extra <code>*/</code>, indicating an end to a comment.  Be sure that there is an opening <code>/*</code> for every closing <code>*/</code>.' ) );
222                         $imbalanced = true;
223                 }
224                 if ( $imbalanced && $this->is_possible_content_error( $css ) ) {
225                         $validity->add( 'possible_false_positive', __( 'Imbalanced/unclosed character errors can be caused by <code>content: "";</code> declarations. You may need to remove this or add it to a custom CSS file.' ) );
226                 }
227
228                 if ( empty( $validity->errors ) ) {
229                         $validity = parent::validate( $css );
230                 }
231                 return $validity;
232         }
233
234         /**
235          * Store the CSS setting value in the custom_css custom post type for the stylesheet.
236          *
237          * @since 4.7.0
238          * @access public
239          *
240          * @param string $css The input value.
241          * @return int|false The post ID or false if the value could not be saved.
242          */
243         public function update( $css ) {
244                 if ( empty( $css ) ) {
245                         $css = '';
246                 }
247
248                 $r = wp_update_custom_css_post( $css, array(
249                         'stylesheet' => $this->stylesheet,
250                 ) );
251
252                 if ( $r instanceof WP_Error ) {
253                         return false;
254                 }
255                 $post_id = $r->ID;
256
257                 // Cache post ID in theme mod for performance to avoid additional DB query.
258                 if ( $this->manager->get_stylesheet() === $this->stylesheet ) {
259                         set_theme_mod( 'custom_css_post_id', $post_id );
260                 }
261
262                 return $post_id;
263         }
264
265         /**
266          * Ensure there are a balanced number of paired characters.
267          *
268          * This is used to check that the number of opening and closing
269          * characters is equal.
270          *
271          * For instance, there should be an equal number of braces ("{", "}")
272          * in the CSS.
273          *
274          * @since 4.7.0
275          * @access private
276          *
277          * @param string $opening_char The opening character.
278          * @param string $closing_char The closing character.
279          * @param string $css The CSS input string.
280          *
281          * @return bool
282          */
283         private function validate_balanced_characters( $opening_char, $closing_char, $css ) {
284                 return substr_count( $css, $opening_char ) === substr_count( $css, $closing_char );
285         }
286
287         /**
288          * Ensure there are an even number of paired characters.
289          *
290          * This is used to check that the number of a specific
291          * character is even.
292          *
293          * For instance, there should be an even number of double quotes
294          * in the CSS.
295          *
296          * @since 4.7.0
297          * @access private
298          *
299          * @param string $char A character.
300          * @param string $css The CSS input string.
301          * @return bool Equality.
302          */
303         private function validate_equal_characters( $char, $css ) {
304                 $char_count = substr_count( $css, $char );
305                 return ( 0 === $char_count % 2 );
306         }
307
308         /**
309          * Count unclosed CSS Comments.
310          *
311          * Used during validation.
312          *
313          * @see self::validate()
314          *
315          * @since 4.7.0
316          * @access private
317          *
318          * @param string $css The CSS input string.
319          * @return int Count.
320          */
321         private function validate_count_unclosed_comments( $css ) {
322                 $count = 0;
323                 $comments = explode( '/*', $css );
324
325                 if ( ! is_array( $comments ) || ( 1 >= count( $comments ) ) ) {
326                         return $count;
327                 }
328
329                 unset( $comments[0] ); // The first item is before the first comment.
330                 foreach ( $comments as $comment ) {
331                         if ( false === strpos( $comment, '*/' ) ) {
332                                 $count++;
333                         }
334                 }
335                 return $count;
336         }
337
338         /**
339          * Find "content:" within a string.
340          *
341          * Imbalanced/Unclosed validation errors may be caused
342          * when a character is used in a "content:" declaration.
343          *
344          * This function is used to detect if this is a possible
345          * cause of the validation error, so that if it is,
346          * a notification may be added to the Validation Errors.
347          *
348          * Example:
349          * .element::before {
350          *   content: "(\"";
351          * }
352          * .element::after {
353          *   content: "\")";
354          * }
355          *
356          * Using ! empty() because strpos() may return non-boolean values
357          * that evaluate to false. This would be problematic when
358          * using a strict "false === strpos()" comparison.
359          *
360          * @since 4.7.0
361          * @access private
362          *
363          * @param string $css The CSS input string.
364          * @return bool
365          */
366         private function is_possible_content_error( $css ) {
367                 $found = preg_match( '/\bcontent\s*:/', $css );
368                 if ( ! empty( $found ) ) {
369                         return true;
370                 }
371                 return false;
372         }
373 }