WordPress 4.7.1
[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 double quotes are equal.
195                 if ( ! $this->validate_equal_characters( '"', $css ) ) {
196                         $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>.' ) );
197                         $imbalanced = true;
198                 }
199
200                 /*
201                  * Make sure any code comments are closed properly.
202                  *
203                  * The first check could miss stray an unpaired comment closing figure, so if
204                  * The number appears to be balanced, then check for equal numbers
205                  * of opening/closing comment figures.
206                  *
207                  * Although it may initially appear redundant, we use the first method
208                  * to give more specific feedback to the user.
209                  */
210                 $unclosed_comment_count = $this->validate_count_unclosed_comments( $css );
211                 if ( 0 < $unclosed_comment_count ) {
212                         $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 ) );
213                         $imbalanced = true;
214                 } elseif ( ! $this->validate_balanced_characters( '/*', '*/', $css ) ) {
215                         $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>.' ) );
216                         $imbalanced = true;
217                 }
218                 if ( $imbalanced && $this->is_possible_content_error( $css ) ) {
219                         $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.' ) );
220                 }
221
222                 if ( empty( $validity->errors ) ) {
223                         $validity = parent::validate( $css );
224                 }
225                 return $validity;
226         }
227
228         /**
229          * Store the CSS setting value in the custom_css custom post type for the stylesheet.
230          *
231          * @since 4.7.0
232          * @access public
233          *
234          * @param string $css The input value.
235          * @return int|false The post ID or false if the value could not be saved.
236          */
237         public function update( $css ) {
238                 if ( empty( $css ) ) {
239                         $css = '';
240                 }
241
242                 $r = wp_update_custom_css_post( $css, array(
243                         'stylesheet' => $this->stylesheet,
244                 ) );
245
246                 if ( $r instanceof WP_Error ) {
247                         return false;
248                 }
249                 $post_id = $r->ID;
250
251                 // Cache post ID in theme mod for performance to avoid additional DB query.
252                 if ( $this->manager->get_stylesheet() === $this->stylesheet ) {
253                         set_theme_mod( 'custom_css_post_id', $post_id );
254                 }
255
256                 return $post_id;
257         }
258
259         /**
260          * Ensure there are a balanced number of paired characters.
261          *
262          * This is used to check that the number of opening and closing
263          * characters is equal.
264          *
265          * For instance, there should be an equal number of braces ("{", "}")
266          * in the CSS.
267          *
268          * @since 4.7.0
269          * @access private
270          *
271          * @param string $opening_char The opening character.
272          * @param string $closing_char The closing character.
273          * @param string $css The CSS input string.
274          *
275          * @return bool
276          */
277         private function validate_balanced_characters( $opening_char, $closing_char, $css ) {
278                 return substr_count( $css, $opening_char ) === substr_count( $css, $closing_char );
279         }
280
281         /**
282          * Ensure there are an even number of paired characters.
283          *
284          * This is used to check that the number of a specific
285          * character is even.
286          *
287          * For instance, there should be an even number of double quotes
288          * in the CSS.
289          *
290          * @since 4.7.0
291          * @access private
292          *
293          * @param string $char A character.
294          * @param string $css The CSS input string.
295          * @return bool Equality.
296          */
297         private function validate_equal_characters( $char, $css ) {
298                 $char_count = substr_count( $css, $char );
299                 return ( 0 === $char_count % 2 );
300         }
301
302         /**
303          * Count unclosed CSS Comments.
304          *
305          * Used during validation.
306          *
307          * @see self::validate()
308          *
309          * @since 4.7.0
310          * @access private
311          *
312          * @param string $css The CSS input string.
313          * @return int Count.
314          */
315         private function validate_count_unclosed_comments( $css ) {
316                 $count = 0;
317                 $comments = explode( '/*', $css );
318
319                 if ( ! is_array( $comments ) || ( 1 >= count( $comments ) ) ) {
320                         return $count;
321                 }
322
323                 unset( $comments[0] ); // The first item is before the first comment.
324                 foreach ( $comments as $comment ) {
325                         if ( false === strpos( $comment, '*/' ) ) {
326                                 $count++;
327                         }
328                 }
329                 return $count;
330         }
331
332         /**
333          * Find "content:" within a string.
334          *
335          * Imbalanced/Unclosed validation errors may be caused
336          * when a character is used in a "content:" declaration.
337          *
338          * This function is used to detect if this is a possible
339          * cause of the validation error, so that if it is,
340          * a notification may be added to the Validation Errors.
341          *
342          * Example:
343          * .element::before {
344          *   content: "(\"";
345          * }
346          * .element::after {
347          *   content: "\")";
348          * }
349          *
350          * Using ! empty() because strpos() may return non-boolean values
351          * that evaluate to false. This would be problematic when
352          * using a strict "false === strpos()" comparison.
353          *
354          * @since 4.7.0
355          * @access private
356          *
357          * @param string $css The CSS input string.
358          * @return bool
359          */
360         private function is_possible_content_error( $css ) {
361                 $found = preg_match( '/\bcontent\s*:/', $css );
362                 if ( ! empty( $found ) ) {
363                         return true;
364                 }
365                 return false;
366         }
367 }