408 lines
11 KiB
PHP
408 lines
11 KiB
PHP
<?php
|
||
/**
|
||
* Customize API: WP_Customize_Custom_CSS_Setting class
|
||
*
|
||
* This handles validation, sanitization and saving of the value.
|
||
*
|
||
* @package WordPress
|
||
* @subpackage Customize
|
||
* @since 4.7.0
|
||
*/
|
||
|
||
/**
|
||
* Custom Setting to handle WP Custom CSS.
|
||
*
|
||
* @since 4.7.0
|
||
*
|
||
* @see WP_Customize_Setting
|
||
*/
|
||
final class WP_Customize_Custom_CSS_Setting extends WP_Customize_Setting {
|
||
|
||
/**
|
||
* The setting type.
|
||
*
|
||
* @since 4.7.0
|
||
* @access public
|
||
* @var string
|
||
*/
|
||
public $type = 'custom_css';
|
||
|
||
/**
|
||
* Setting Transport
|
||
*
|
||
* @since 4.7.0
|
||
* @access public
|
||
* @var string
|
||
*/
|
||
public $transport = 'postMessage';
|
||
|
||
/**
|
||
* Capability required to edit this setting.
|
||
*
|
||
* @since 4.7.0
|
||
* @access public
|
||
* @var string
|
||
*/
|
||
public $capability = 'edit_css';
|
||
|
||
/**
|
||
* Stylesheet
|
||
*
|
||
* @since 4.7.0
|
||
* @access public
|
||
* @var string
|
||
*/
|
||
public $stylesheet = '';
|
||
|
||
/**
|
||
* WP_Customize_Custom_CSS_Setting constructor.
|
||
*
|
||
* @since 4.7.0
|
||
* @access public
|
||
*
|
||
* @throws Exception If the setting ID does not match the pattern `custom_css[$stylesheet]`.
|
||
*
|
||
* @param WP_Customize_Manager $manager The Customize Manager class.
|
||
* @param string $id An specific ID of the setting. Can be a
|
||
* theme mod or option name.
|
||
* @param array $args Setting arguments.
|
||
*/
|
||
public function __construct( $manager, $id, $args = array() ) {
|
||
parent::__construct( $manager, $id, $args );
|
||
if ( 'custom_css' !== $this->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( '#</?\w+#', $css ) ) {
|
||
$validity->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', sprintf(
|
||
/* translators: 1: {}, 2: }, 3: { */
|
||
__( 'Your curly brackets %1$s are imbalanced. Make sure there is a closing %2$s for every opening %3$s.' ),
|
||
'<code>{}</code>',
|
||
'<code>}</code>',
|
||
'<code>{</code>'
|
||
) );
|
||
$imbalanced = true;
|
||
}
|
||
|
||
// Ensure brackets are balanced.
|
||
if ( ! $this->validate_balanced_characters( '[', ']', $css ) ) {
|
||
$validity->add( 'imbalanced_braces', sprintf(
|
||
/* translators: 1: [], 2: ], 3: [ */
|
||
__( 'Your brackets %1$s are imbalanced. Make sure there is a closing %2$s for every opening %3$s.' ),
|
||
'<code>[]</code>',
|
||
'<code>]</code>',
|
||
'<code>[</code>'
|
||
) );
|
||
$imbalanced = true;
|
||
}
|
||
|
||
// Ensure parentheses are balanced.
|
||
if ( ! $this->validate_balanced_characters( '(', ')', $css ) ) {
|
||
$validity->add( 'imbalanced_parentheses', sprintf(
|
||
/* translators: 1: (), 2: ), 3: ( */
|
||
__( 'Your parentheses %1$s are imbalanced. Make sure there is a closing %2$s for every opening %3$s.' ),
|
||
'<code>()</code>',
|
||
'<code>)</code>',
|
||
'<code>(</code>'
|
||
) );
|
||
$imbalanced = true;
|
||
}
|
||
|
||
// Ensure double quotes are equal.
|
||
if ( ! $this->validate_equal_characters( '"', $css ) ) {
|
||
$validity->add( 'unequal_double_quotes', sprintf(
|
||
/* translators: 1: " (double quote) */
|
||
__( 'Your double quotes %1$s are uneven. Make sure there is a closing %1$s for every opening %1$s.' ),
|
||
'<code>"</code>'
|
||
) );
|
||
$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(
|
||
/* translators: 1: number of unclosed comments, 2: */ */
|
||
_n(
|
||
'There is %1$s unclosed code comment. Close each comment with %2$s.',
|
||
'There are %1$s unclosed code comments. Close each comment with %2$s.',
|
||
$unclosed_comment_count
|
||
),
|
||
$unclosed_comment_count,
|
||
'<code>*/</code>'
|
||
) );
|
||
$imbalanced = true;
|
||
} elseif ( ! $this->validate_balanced_characters( '/*', '*/', $css ) ) {
|
||
$validity->add( 'imbalanced_comments', sprintf(
|
||
/* translators: 1: */, 2: /* */
|
||
__( 'There is an extra %1$s, indicating an end to a comment. Be sure that there is an opening %2$s for every closing %1$s.' ),
|
||
'<code>*/</code>',
|
||
'<code>/*</code>'
|
||
) );
|
||
$imbalanced = true;
|
||
}
|
||
if ( $imbalanced && $this->is_possible_content_error( $css ) ) {
|
||
$validity->add( 'possible_false_positive', sprintf(
|
||
/* translators: %s: content: ""; */
|
||
__( 'Imbalanced/unclosed character errors can be caused by %s declarations. You may need to remove this or add it to a custom CSS file.' ),
|
||
'<code>content: "";</code>'
|
||
) );
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|