id_data['base'] ) { throw new Exception( 'Expected custom_css id_base.' ); } if ( 1 !== count( $this->id_data['keys'] ) || empty( $this->id_data['keys'][0] ) ) { throw new Exception( 'Expected single stylesheet key.' ); } $this->stylesheet = $this->id_data['keys'][0]; } /** * Add filter to preview post value. * * @since 4.7.9 * @access public * * @return bool False when preview short-circuits due no change needing to be previewed. */ public function preview() { if ( $this->is_previewed ) { return false; } $this->is_previewed = true; add_filter( 'wp_get_custom_css', array( $this, 'filter_previewed_wp_get_custom_css' ), 9, 2 ); return true; } /** * Filter `wp_get_custom_css` for applying the customized value. * * This is used in the preview when `wp_get_custom_css()` is called for rendering the styles. * * @since 4.7.0 * @access private * @see wp_get_custom_css() * * @param string $css Original CSS. * @param string $stylesheet Current stylesheet. * @return string CSS. */ public function filter_previewed_wp_get_custom_css( $css, $stylesheet ) { if ( $stylesheet === $this->stylesheet ) { $customized_value = $this->post_value( null ); if ( ! is_null( $customized_value ) ) { $css = $customized_value; } } return $css; } /** * Fetch the value of the setting. Will return the previewed value when `preview()` is called. * * @since 4.7.0 * @access public * @see WP_Customize_Setting::value() * * @return string */ public function value() { if ( $this->is_previewed ) { $post_value = $this->post_value( null ); if ( null !== $post_value ) { return $post_value; } } $id_base = $this->id_data['base']; $value = ''; $post = wp_get_custom_css_post( $this->stylesheet ); if ( $post ) { $value = $post->post_content; } if ( empty( $value ) ) { $value = $this->default; } /** This filter is documented in wp-includes/class-wp-customize-setting.php */ $value = apply_filters( "customize_value_{$id_base}", $value, $this ); return $value; } /** * Validate CSS. * * Checks for imbalanced braces, brackets, and comments. * Notifications are rendered when the customizer state is saved. * * @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. * * @since 4.7.0 * @access public * * @param string $css The input string. * @return true|WP_Error True if the input was validated, otherwise WP_Error. */ public function validate( $css ) { $validity = new WP_Error(); if ( preg_match( '#add( 'illegal_markup', __( 'Markup is not allowed in CSS.' ) ); } $imbalanced = false; // Make sure that there is a closing brace for each opening brace. if ( ! $this->validate_balanced_characters( '{', '}', $css ) ) { $validity->add( 'imbalanced_curly_brackets', __( 'Your curly brackets {} are imbalanced. Make sure there is a closing } for every opening {.' ) ); $imbalanced = true; } // Ensure brackets are balanced. if ( ! $this->validate_balanced_characters( '[', ']', $css ) ) { $validity->add( 'imbalanced_braces', __( 'Your brackets [] are imbalanced. Make sure there is a closing ] for every opening [.' ) ); $imbalanced = true; } // Ensure parentheses are balanced. if ( ! $this->validate_balanced_characters( '(', ')', $css ) ) { $validity->add( 'imbalanced_parentheses', __( 'Your parentheses () are imbalanced. Make sure there is a closing ) for every opening (.' ) ); $imbalanced = true; } // Ensure single quotes are equal. if ( ! $this->validate_equal_characters( '\'', $css ) ) { $validity->add( 'unequal_single_quotes', __( 'Your single quotes \' are uneven. Make sure there is a closing \' for every opening \'.' ) ); $imbalanced = true; } // Ensure single quotes are equal. if ( ! $this->validate_equal_characters( '"', $css ) ) { $validity->add( 'unequal_double_quotes', __( 'Your double quotes " are uneven. Make sure there is a closing " for every opening ".' ) ); $imbalanced = true; } /* * Make sure any code comments are closed properly. * * The first check could miss stray an unpaired comment closing figure, so if * The number appears to be balanced, then check for equal numbers * of opening/closing comment figures. * * Although it may initially appear redundant, we use the first method * to give more specific feedback to the user. */ $unclosed_comment_count = $this->validate_count_unclosed_comments( $css ); if ( 0 < $unclosed_comment_count ) { $validity->add( 'unclosed_comment', sprintf( _n( 'There is %s unclosed code comment. Close each comment with */.', 'There are %s unclosed code comments. Close each comment with */.', $unclosed_comment_count ), $unclosed_comment_count ) ); $imbalanced = true; } elseif ( ! $this->validate_balanced_characters( '/*', '*/', $css ) ) { $validity->add( 'imbalanced_comments', __( 'There is an extra */, indicating an end to a comment. Be sure that there is an opening /* for every closing */.' ) ); $imbalanced = true; } if ( $imbalanced && $this->is_possible_content_error( $css ) ) { $validity->add( 'possible_false_positive', __( 'Imbalanced/unclosed character errors can be caused by content: ""; declarations. You may need to remove this or add it to a custom CSS file.' ) ); } if ( empty( $validity->errors ) ) { $validity = parent::validate( $css ); } return $validity; } /** * Store the CSS setting value in the custom_css custom post type for the stylesheet. * * @since 4.7.0 * @access public * * @param string $css The input value. * @return int|false The post ID or false if the value could not be saved. */ public function update( $css ) { if ( empty( $css ) ) { $css = ''; } $r = wp_update_custom_css_post( $css, array( 'stylesheet' => $this->stylesheet, ) ); if ( $r instanceof WP_Error ) { return false; } $post_id = $r->ID; // Cache post ID in theme mod for performance to avoid additional DB query. if ( $this->manager->get_stylesheet() === $this->stylesheet ) { set_theme_mod( 'custom_css_post_id', $post_id ); } return $post_id; } /** * Ensure there are a balanced number of paired characters. * * This is used to check that the number of opening and closing * characters is equal. * * For instance, there should be an equal number of braces ("{", "}") * in the CSS. * * @since 4.7.0 * @access private * * @param string $opening_char The opening character. * @param string $closing_char The closing character. * @param string $css The CSS input string. * * @return bool */ private function validate_balanced_characters( $opening_char, $closing_char, $css ) { return substr_count( $css, $opening_char ) === substr_count( $css, $closing_char ); } /** * Ensure there are an even number of paired characters. * * This is used to check that the number of a specific * character is even. * * For instance, there should be an even number of double quotes * in the CSS. * * @since 4.7.0 * @access private * * @param string $char A character. * @param string $css The CSS input string. * @return bool Equality. */ private function validate_equal_characters( $char, $css ) { $char_count = substr_count( $css, $char ); return ( 0 === $char_count % 2 ); } /** * Count unclosed CSS Comments. * * Used during validation. * * @see self::validate() * * @since 4.7.0 * @access private * * @param string $css The CSS input string. * @return int Count. */ private function validate_count_unclosed_comments( $css ) { $count = 0; $comments = explode( '/*', $css ); if ( ! is_array( $comments ) || ( 1 >= count( $comments ) ) ) { return $count; } unset( $comments[0] ); // The first item is before the first comment. foreach ( $comments as $comment ) { if ( false === strpos( $comment, '*/' ) ) { $count++; } } return $count; } /** * Find "content:" within a string. * * Imbalanced/Unclosed validation errors may be caused * when a character is used in a "content:" declaration. * * This function is used to detect if this is a possible * cause of the validation error, so that if it is, * a notification may be added to the Validation Errors. * * Example: * .element::before { * content: "(\""; * } * .element::after { * content: "\")"; * } * * Using ! empty() because strpos() may return non-boolean values * that evaluate to false. This would be problematic when * using a strict "false === strpos()" comparison. * * @since 4.7.0 * @access private * * @param string $css The CSS input string. * @return bool */ private function is_possible_content_error( $css ) { $found = preg_match( '/\bcontent\s*:/', $css ); if ( ! empty( $found ) ) { return true; } return false; } }