WordPress Version: 6.5
/**
* Filters an inline style attribute and removes disallowed rules.
*
* @since 2.8.1
* @since 4.4.0 Added support for `min-height`, `max-height`, `min-width`, and `max-width`.
* @since 4.6.0 Added support for `list-style-type`.
* @since 5.0.0 Added support for `background-image`.
* @since 5.1.0 Added support for `text-transform`.
* @since 5.2.0 Added support for `background-position` and `grid-template-columns`.
* @since 5.3.0 Added support for `grid`, `flex` and `column` layout properties.
* Extended `background-*` support for individual properties.
* @since 5.3.1 Added support for gradient backgrounds.
* @since 5.7.1 Added support for `object-position`.
* @since 5.8.0 Added support for `calc()` and `var()` values.
* @since 6.1.0 Added support for `min()`, `max()`, `minmax()`, `clamp()`,
* nested `var()` values, and assigning values to CSS variables.
* Added support for `object-fit`, `gap`, `column-gap`, `row-gap`, and `flex-wrap`.
* Extended `margin-*` and `padding-*` support for logical properties.
* @since 6.2.0 Added support for `aspect-ratio`, `position`, `top`, `right`, `bottom`, `left`,
* and `z-index` CSS properties.
* @since 6.3.0 Extended support for `filter` to accept a URL and added support for repeat().
* Added support for `box-shadow`.
* @since 6.4.0 Added support for `writing-mode`.
* @since 6.5.0 Added support for `background-repeat`.
*
* @param string $css A string of CSS rules.
* @param string $deprecated Not used.
* @return string Filtered string of CSS rules.
*/
function safecss_filter_attr($css, $deprecated = '')
{
if (!empty($deprecated)) {
_deprecated_argument(__FUNCTION__, '2.8.1');
// Never implemented.
}
$css = wp_kses_no_null($css);
$css = str_replace(array("\n", "\r", "\t"), '', $css);
$allowed_protocols = wp_allowed_protocols();
$css_array = explode(';', trim($css));
/**
* Filters the list of allowed CSS attributes.
*
* @since 2.8.1
*
* @param string[] $attr Array of allowed CSS attributes.
*/
$allowed_attr = apply_filters('safe_style_css', array(
'background',
'background-color',
'background-image',
'background-position',
'background-repeat',
'background-size',
'background-attachment',
'background-blend-mode',
'border',
'border-radius',
'border-width',
'border-color',
'border-style',
'border-right',
'border-right-color',
'border-right-style',
'border-right-width',
'border-bottom',
'border-bottom-color',
'border-bottom-left-radius',
'border-bottom-right-radius',
'border-bottom-style',
'border-bottom-width',
'border-bottom-right-radius',
'border-bottom-left-radius',
'border-left',
'border-left-color',
'border-left-style',
'border-left-width',
'border-top',
'border-top-color',
'border-top-left-radius',
'border-top-right-radius',
'border-top-style',
'border-top-width',
'border-top-left-radius',
'border-top-right-radius',
'border-spacing',
'border-collapse',
'caption-side',
'columns',
'column-count',
'column-fill',
'column-gap',
'column-rule',
'column-span',
'column-width',
'color',
'filter',
'font',
'font-family',
'font-size',
'font-style',
'font-variant',
'font-weight',
'letter-spacing',
'line-height',
'text-align',
'text-decoration',
'text-indent',
'text-transform',
'height',
'min-height',
'max-height',
'width',
'min-width',
'max-width',
'margin',
'margin-right',
'margin-bottom',
'margin-left',
'margin-top',
'margin-block-start',
'margin-block-end',
'margin-inline-start',
'margin-inline-end',
'padding',
'padding-right',
'padding-bottom',
'padding-left',
'padding-top',
'padding-block-start',
'padding-block-end',
'padding-inline-start',
'padding-inline-end',
'flex',
'flex-basis',
'flex-direction',
'flex-flow',
'flex-grow',
'flex-shrink',
'flex-wrap',
'gap',
'column-gap',
'row-gap',
'grid-template-columns',
'grid-auto-columns',
'grid-column-start',
'grid-column-end',
'grid-column-gap',
'grid-template-rows',
'grid-auto-rows',
'grid-row-start',
'grid-row-end',
'grid-row-gap',
'grid-gap',
'justify-content',
'justify-items',
'justify-self',
'align-content',
'align-items',
'align-self',
'clear',
'cursor',
'direction',
'float',
'list-style-type',
'object-fit',
'object-position',
'overflow',
'vertical-align',
'writing-mode',
'position',
'top',
'right',
'bottom',
'left',
'z-index',
'box-shadow',
'aspect-ratio',
// Custom CSS properties.
'--*',
));
/*
* CSS attributes that accept URL data types.
*
* This is in accordance to the CSS spec and unrelated to
* the sub-set of supported attributes above.
*
* See: https://developer.mozilla.org/en-US/docs/Web/CSS/url
*/
$css_url_data_types = array('background', 'background-image', 'cursor', 'filter', 'list-style', 'list-style-image');
/*
* CSS attributes that accept gradient data types.
*
*/
$css_gradient_data_types = array('background', 'background-image');
if (empty($allowed_attr)) {
return $css;
}
$css = '';
foreach ($css_array as $css_item) {
if ('' === $css_item) {
continue;
}
$css_item = trim($css_item);
$css_test_string = $css_item;
$found = false;
$url_attr = false;
$gradient_attr = false;
$is_custom_var = false;
if (!str_contains($css_item, ':')) {
$found = true;
} else {
$parts = explode(':', $css_item, 2);
$css_selector = trim($parts[0]);
// Allow assigning values to CSS variables.
if (in_array('--*', $allowed_attr, true) && preg_match('/^--[a-zA-Z0-9-_]+$/', $css_selector)) {
$allowed_attr[] = $css_selector;
$is_custom_var = true;
}
if (in_array($css_selector, $allowed_attr, true)) {
$found = true;
$url_attr = in_array($css_selector, $css_url_data_types, true);
$gradient_attr = in_array($css_selector, $css_gradient_data_types, true);
}
if ($is_custom_var) {
$css_value = trim($parts[1]);
$url_attr = str_starts_with($css_value, 'url(');
$gradient_attr = str_contains($css_value, '-gradient(');
}
}
if ($found && $url_attr) {
// Simplified: matches the sequence `url(*)`.
preg_match_all('/url\([^)]+\)/', $parts[1], $url_matches);
foreach ($url_matches[0] as $url_match) {
// Clean up the URL from each of the matches above.
preg_match('/^url\(\s*([\'\"]?)(.*)(\g1)\s*\)$/', $url_match, $url_pieces);
if (empty($url_pieces[2])) {
$found = false;
break;
}
$url = trim($url_pieces[2]);
if (empty($url) || wp_kses_bad_protocol($url, $allowed_protocols) !== $url) {
$found = false;
break;
} else {
// Remove the whole `url(*)` bit that was matched above from the CSS.
$css_test_string = str_replace($url_match, '', $css_test_string);
}
}
}
if ($found && $gradient_attr) {
$css_value = trim($parts[1]);
if (preg_match('/^(repeating-)?(linear|radial|conic)-gradient\(([^()]|rgb[a]?\([^()]*\))*\)$/', $css_value)) {
// Remove the whole `gradient` bit that was matched above from the CSS.
$css_test_string = str_replace($css_value, '', $css_test_string);
}
}
if ($found) {
/*
* Allow CSS functions like var(), calc(), etc. by removing them from the test string.
* Nested functions and parentheses are also removed, so long as the parentheses are balanced.
*/
$css_test_string = preg_replace('/\b(?:var|calc|min|max|minmax|clamp|repeat)(\((?:[^()]|(?1))*\))/', '', $css_test_string);
/*
* Disallow CSS containing \ ( & } = or comments, except for within url(), var(), calc(), etc.
* which were removed from the test string above.
*/
$allow_css = !preg_match('%[\\\\(&=}]|/\*%', $css_test_string);
/**
* Filters the check for unsafe CSS in `safecss_filter_attr`.
*
* Enables developers to determine whether a section of CSS should be allowed or discarded.
* By default, the value will be false if the part contains \ ( & } = or comments.
* Return true to allow the CSS part to be included in the output.
*
* @since 5.5.0
*
* @param bool $allow_css Whether the CSS in the test string is considered safe.
* @param string $css_test_string The CSS string to test.
*/
$allow_css = apply_filters('safecss_filter_attr_allow_css', $allow_css, $css_test_string);
// Only add the CSS part if it passes the regex check.
if ($allow_css) {
if ('' !== $css) {
$css .= ';';
}
$css .= $css_item;
}
}
}
return $css;
}