3 * Customize API: WP_Customize_Custom_CSS_Setting class
5 * This handles validation, sanitization and saving of the value.
8 * @subpackage Customize
13 * Custom Setting to handle WP Custom CSS.
17 * @see WP_Customize_Setting
19 final class WP_Customize_Custom_CSS_Setting extends WP_Customize_Setting {
28 public $type = 'custom_css';
37 public $transport = 'postMessage';
40 * Capability required to edit this setting.
46 public $capability = 'edit_css';
55 public $stylesheet = '';
58 * WP_Customize_Custom_CSS_Setting constructor.
63 * @throws Exception If the setting ID does not match the pattern `custom_css[$stylesheet]`.
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.
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.' );
75 if ( 1 !== count( $this->id_data['keys'] ) || empty( $this->id_data['keys'][0] ) ) {
76 throw new Exception( 'Expected single stylesheet key.' );
78 $this->stylesheet = $this->id_data['keys'][0];
82 * Add filter to preview post value.
87 * @return bool False when preview short-circuits due no change needing to be previewed.
89 public function preview() {
90 if ( $this->is_previewed ) {
93 $this->is_previewed = true;
94 add_filter( 'wp_get_custom_css', array( $this, 'filter_previewed_wp_get_custom_css' ), 9, 2 );
99 * Filter `wp_get_custom_css` for applying the customized value.
101 * This is used in the preview when `wp_get_custom_css()` is called for rendering the styles.
105 * @see wp_get_custom_css()
107 * @param string $css Original CSS.
108 * @param string $stylesheet Current stylesheet.
109 * @return string CSS.
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;
122 * Fetch the value of the setting. Will return the previewed value when `preview()` is called.
126 * @see WP_Customize_Setting::value()
130 public function value() {
131 if ( $this->is_previewed ) {
132 $post_value = $this->post_value( null );
133 if ( null !== $post_value ) {
137 $id_base = $this->id_data['base'];
139 $post = wp_get_custom_css_post( $this->stylesheet );
141 $value = $post->post_content;
143 if ( empty( $value ) ) {
144 $value = $this->default;
147 /** This filter is documented in wp-includes/class-wp-customize-setting.php */
148 $value = apply_filters( "customize_value_{$id_base}", $value, $this );
156 * Checks for imbalanced braces, brackets, and comments.
157 * Notifications are rendered when the customizer state is saved.
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.
164 * @param string $css The input string.
165 * @return true|WP_Error True if the input was validated, otherwise WP_Error.
167 public function validate( $css ) {
168 $validity = new WP_Error();
170 if ( preg_match( '#</?\w+#', $css ) ) {
171 $validity->add( 'illegal_markup', __( 'Markup is not allowed in CSS.' ) );
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>.' ) );
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>.' ) );
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>.' ) );
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>.' ) );
201 * Make sure any code comments are closed properly.
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.
207 * Although it may initially appear redundant, we use the first method
208 * to give more specific feedback to the user.
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 ) );
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>.' ) );
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.' ) );
222 if ( empty( $validity->errors ) ) {
223 $validity = parent::validate( $css );
229 * Store the CSS setting value in the custom_css custom post type for the stylesheet.
234 * @param string $css The input value.
235 * @return int|false The post ID or false if the value could not be saved.
237 public function update( $css ) {
238 if ( empty( $css ) ) {
242 $r = wp_update_custom_css_post( $css, array(
243 'stylesheet' => $this->stylesheet,
246 if ( $r instanceof WP_Error ) {
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 );
260 * Ensure there are a balanced number of paired characters.
262 * This is used to check that the number of opening and closing
263 * characters is equal.
265 * For instance, there should be an equal number of braces ("{", "}")
271 * @param string $opening_char The opening character.
272 * @param string $closing_char The closing character.
273 * @param string $css The CSS input string.
277 private function validate_balanced_characters( $opening_char, $closing_char, $css ) {
278 return substr_count( $css, $opening_char ) === substr_count( $css, $closing_char );
282 * Ensure there are an even number of paired characters.
284 * This is used to check that the number of a specific
287 * For instance, there should be an even number of double quotes
293 * @param string $char A character.
294 * @param string $css The CSS input string.
295 * @return bool Equality.
297 private function validate_equal_characters( $char, $css ) {
298 $char_count = substr_count( $css, $char );
299 return ( 0 === $char_count % 2 );
303 * Count unclosed CSS Comments.
305 * Used during validation.
307 * @see self::validate()
312 * @param string $css The CSS input string.
315 private function validate_count_unclosed_comments( $css ) {
317 $comments = explode( '/*', $css );
319 if ( ! is_array( $comments ) || ( 1 >= count( $comments ) ) ) {
323 unset( $comments[0] ); // The first item is before the first comment.
324 foreach ( $comments as $comment ) {
325 if ( false === strpos( $comment, '*/' ) ) {
333 * Find "content:" within a string.
335 * Imbalanced/Unclosed validation errors may be caused
336 * when a character is used in a "content:" declaration.
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.
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.
357 * @param string $css The CSS input string.
360 private function is_possible_content_error( $css ) {
361 $found = preg_match( '/\bcontent\s*:/', $css );
362 if ( ! empty( $found ) ) {