You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
899 lines
25 KiB
PHTML
899 lines
25 KiB
PHTML
8 months ago
|
<?php
|
||
|
/**
|
||
|
* Class KSES.
|
||
|
*
|
||
|
* @link https://github.com/googleforcreators/web-stories-wp
|
||
|
*
|
||
|
* @copyright 2020 Google LLC
|
||
|
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Copyright 2020 Google LLC
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
|
||
|
declare(strict_types = 1);
|
||
|
|
||
|
namespace Google\Web_Stories;
|
||
|
|
||
|
use Google\Web_Stories\Infrastructure\HasRequirements;
|
||
|
|
||
|
/**
|
||
|
* KSES class.
|
||
|
*
|
||
|
* Provides KSES utility methods to override the ones from core.
|
||
|
*
|
||
|
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
|
||
|
*
|
||
|
* @phpstan-type PostData array{
|
||
|
* post_parent: int|string|null,
|
||
|
* post_type: string,
|
||
|
* post_content?: string,
|
||
|
* post_content_filtered?: string
|
||
|
* }
|
||
|
*/
|
||
|
class KSES extends Service_Base implements HasRequirements {
|
||
|
|
||
|
/**
|
||
|
* Story_Post_Type instance.
|
||
|
*
|
||
|
* @var Story_Post_Type Story_Post_Type instance.
|
||
|
*/
|
||
|
private Story_Post_Type $story_post_type;
|
||
|
|
||
|
/**
|
||
|
* Page_Template_Post_Type instance.
|
||
|
*
|
||
|
* @var Page_Template_Post_Type Page_Template_Post_Type instance.
|
||
|
*/
|
||
|
private Page_Template_Post_Type $page_template_post_type;
|
||
|
|
||
|
/**
|
||
|
* KSES constructor.
|
||
|
*
|
||
|
* @since 1.12.0
|
||
|
*
|
||
|
* @param Story_Post_Type $story_post_type Story_Post_Type instance.
|
||
|
* @param Page_Template_Post_Type $page_template_post_type Page_Template_Post_Type instance.
|
||
|
*/
|
||
|
public function __construct(
|
||
|
Story_Post_Type $story_post_type,
|
||
|
Page_Template_Post_Type $page_template_post_type
|
||
|
) {
|
||
|
$this->story_post_type = $story_post_type;
|
||
|
$this->page_template_post_type = $page_template_post_type;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Initializes KSES filters for stories.
|
||
|
*
|
||
|
* @since 1.0.0
|
||
|
*/
|
||
|
public function register(): void {
|
||
|
add_filter( 'wp_insert_post_data', [ $this, 'filter_insert_post_data' ], 10, 3 );
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the list of service IDs required for this service to be registered.
|
||
|
*
|
||
|
* Needed because the story post type needs to be registered first.
|
||
|
*
|
||
|
* @since 1.13.0
|
||
|
*
|
||
|
* @return string[] List of required services.
|
||
|
*/
|
||
|
public static function get_requirements(): array {
|
||
|
return [ 'story_post_type', 'page_template_post_type' ];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Filters slashed post data just before it is inserted into the database.
|
||
|
*
|
||
|
* Used to run story HTML markup through KSES on our own, but with some filters applied
|
||
|
* that should only affect the web-story post type.
|
||
|
*
|
||
|
* This allows storing full AMP HTML documents in post_content for stories, which require
|
||
|
* more allowed HTML tags and a patched version of {@see safecss_filter_attr}.
|
||
|
*
|
||
|
* @since 1.8.0
|
||
|
*
|
||
|
* @param mixed $data An array of slashed, sanitized, and processed post data.
|
||
|
* @param mixed $postarr An array of sanitized (and slashed) but otherwise unmodified post data.
|
||
|
* @param mixed $unsanitized_postarr An array of slashed yet *unsanitized* and unprocessed post data as
|
||
|
* originally passed to wp_insert_post().
|
||
|
* @return array<string,mixed>|mixed Filtered post data.
|
||
|
*
|
||
|
* @phpstan-param PostData $data
|
||
|
* @phpstan-param PostData $unsanitized_postarr
|
||
|
*
|
||
|
* @template T
|
||
|
*
|
||
|
* @phpstan-return ($data is array<T> ? array<T> : mixed)
|
||
|
*/
|
||
|
public function filter_insert_post_data( $data, $postarr, $unsanitized_postarr ) {
|
||
|
if ( current_user_can( 'unfiltered_html' ) ) {
|
||
|
return $data;
|
||
|
}
|
||
|
|
||
|
if ( ! \is_array( $data ) || ! \is_array( $postarr ) || ! \is_array( $unsanitized_postarr ) ) {
|
||
|
return $data;
|
||
|
}
|
||
|
|
||
|
if ( ! $this->is_allowed_post_type( $data['post_type'], $data['post_parent'] ) ) {
|
||
|
return $data;
|
||
|
}
|
||
|
|
||
|
if ( isset( $unsanitized_postarr['post_content_filtered'] ) ) {
|
||
|
$data['post_content_filtered'] = $this->filter_story_data( $unsanitized_postarr['post_content_filtered'] );
|
||
|
}
|
||
|
|
||
|
if ( isset( $unsanitized_postarr['post_content'] ) ) {
|
||
|
add_filter( 'safe_style_css', [ $this, 'filter_safe_style_css' ] );
|
||
|
add_filter( 'wp_kses_allowed_html', [ $this, 'filter_kses_allowed_html' ] );
|
||
|
|
||
|
$unsanitized_postarr['post_content'] = $this->filter_content_save_pre_before_kses( $unsanitized_postarr['post_content'] );
|
||
|
|
||
|
$data['post_content'] = wp_filter_post_kses( $unsanitized_postarr['post_content'] );
|
||
|
$data['post_content'] = $this->filter_content_save_pre_after_kses( $data['post_content'] );
|
||
|
|
||
|
remove_filter( 'safe_style_css', [ $this, 'filter_safe_style_css' ] );
|
||
|
remove_filter( 'wp_kses_allowed_html', [ $this, 'filter_kses_allowed_html' ] );
|
||
|
}
|
||
|
|
||
|
return $data;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Filters list of allowed CSS attributes.
|
||
|
*
|
||
|
* @since 1.0.0
|
||
|
*
|
||
|
* @param string[]|mixed $attr Array of allowed CSS attributes.
|
||
|
* @return string[]|mixed Filtered list of CSS attributes.
|
||
|
*
|
||
|
* @template T
|
||
|
*
|
||
|
* @phpstan-return ($attr is array<T> ? array<T> : mixed)
|
||
|
*/
|
||
|
public function filter_safe_style_css( $attr ) {
|
||
|
if ( ! \is_array( $attr ) ) {
|
||
|
return $attr;
|
||
|
}
|
||
|
|
||
|
$additional = [
|
||
|
'display',
|
||
|
'opacity',
|
||
|
'position',
|
||
|
'top',
|
||
|
'left',
|
||
|
'transform',
|
||
|
'white-space',
|
||
|
'clip-path',
|
||
|
'-webkit-clip-path',
|
||
|
'pointer-events',
|
||
|
'will-change',
|
||
|
'--initial-opacity',
|
||
|
'--initial-transform',
|
||
|
];
|
||
|
|
||
|
array_push( $attr, ...$additional );
|
||
|
|
||
|
return $attr;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Filters an inline style attribute and removes disallowed rules.
|
||
|
*
|
||
|
* This is equivalent to the WordPress core function of the same name,
|
||
|
* except that this does not remove CSS with parentheses in it.
|
||
|
*
|
||
|
* A few more allowed attributes are added via the safe_style_css filter.
|
||
|
*
|
||
|
* @SuppressWarnings(PHPMD)
|
||
|
*
|
||
|
* @since 1.0.0
|
||
|
*
|
||
|
* @see safecss_filter_attr()
|
||
|
*
|
||
|
* @param string $css A string of CSS rules.
|
||
|
* @return string Filtered string of CSS rules.
|
||
|
*/
|
||
|
public function safecss_filter_attr( string $css ): string { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
|
||
|
$css = wp_kses_no_null( $css );
|
||
|
$css = str_replace( [ "\n", "\r", "\t" ], '', $css );
|
||
|
|
||
|
$allowed_protocols = wp_allowed_protocols();
|
||
|
|
||
|
$css_array = explode( ';', trim( $css ) );
|
||
|
|
||
|
/** This filter is documented in wp-includes/kses.php */
|
||
|
$allowed_attr = apply_filters(
|
||
|
'safe_style_css',
|
||
|
[
|
||
|
'background',
|
||
|
'background-color',
|
||
|
'background-image',
|
||
|
'background-position',
|
||
|
'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-style',
|
||
|
'border-bottom-width',
|
||
|
'border-left',
|
||
|
'border-left-color',
|
||
|
'border-left-style',
|
||
|
'border-left-width',
|
||
|
'border-top',
|
||
|
'border-top-color',
|
||
|
'border-top-style',
|
||
|
'border-top-width',
|
||
|
|
||
|
'border-spacing',
|
||
|
'border-collapse',
|
||
|
'caption-side',
|
||
|
|
||
|
'columns',
|
||
|
'column-count',
|
||
|
'column-fill',
|
||
|
'column-gap',
|
||
|
'column-rule',
|
||
|
'column-span',
|
||
|
'column-width',
|
||
|
|
||
|
'color',
|
||
|
'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',
|
||
|
|
||
|
'padding',
|
||
|
'padding-right',
|
||
|
'padding-bottom',
|
||
|
'padding-left',
|
||
|
'padding-top',
|
||
|
|
||
|
'flex',
|
||
|
'flex-basis',
|
||
|
'flex-direction',
|
||
|
'flex-flow',
|
||
|
'flex-grow',
|
||
|
'flex-shrink',
|
||
|
|
||
|
'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',
|
||
|
'overflow',
|
||
|
'vertical-align',
|
||
|
'list-style-type',
|
||
|
|
||
|
'z-index',
|
||
|
]
|
||
|
);
|
||
|
|
||
|
/*
|
||
|
* 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 = [
|
||
|
'background',
|
||
|
'background-image',
|
||
|
|
||
|
'cursor',
|
||
|
|
||
|
'list-style',
|
||
|
'list-style-image',
|
||
|
|
||
|
'clip-path',
|
||
|
'-webkit-clip-path',
|
||
|
];
|
||
|
|
||
|
/*
|
||
|
* CSS attributes that accept gradient data types.
|
||
|
*
|
||
|
*/
|
||
|
$css_gradient_data_types = [
|
||
|
'background',
|
||
|
'background-image',
|
||
|
];
|
||
|
|
||
|
/*
|
||
|
* CSS attributes that accept color 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/color_value
|
||
|
*/
|
||
|
$css_color_data_types = [
|
||
|
'color',
|
||
|
'background',
|
||
|
'background-color',
|
||
|
'border-color',
|
||
|
'box-shadow',
|
||
|
'outline',
|
||
|
'outline-color',
|
||
|
'text-shadow',
|
||
|
];
|
||
|
|
||
|
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;
|
||
|
$color_attr = false;
|
||
|
$transform_attr = false;
|
||
|
|
||
|
$parts = explode( ':', $css_item, 2 );
|
||
|
|
||
|
if ( ! str_contains( $css_item, ':' ) ) {
|
||
|
$found = true;
|
||
|
} else {
|
||
|
$css_selector = trim( $parts[0] );
|
||
|
|
||
|
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 );
|
||
|
$color_attr = \in_array( $css_selector, $css_color_data_types, true );
|
||
|
|
||
|
// --initial-transform is a special custom property used by the story editor.
|
||
|
$transform_attr = 'transform' === $css_selector || '--initial-transform' === $css_selector;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( $found && $url_attr ) {
|
||
|
$url_matches = [];
|
||
|
|
||
|
// Simplified: matches the sequence `url(*)`.
|
||
|
preg_match_all( '/url\([^)]+\)/', $parts[1], $url_matches );
|
||
|
|
||
|
foreach ( $url_matches[0] as $url_match ) {
|
||
|
$url_pieces = [];
|
||
|
|
||
|
// 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;
|
||
|
}
|
||
|
|
||
|
// 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 && $color_attr ) {
|
||
|
$color_matches = [];
|
||
|
|
||
|
// Simplified: matches the sequence `rgb(*)` and `rgba(*)`.
|
||
|
preg_match_all( '/rgba?\([^)]+\)/', $parts[1], $color_matches );
|
||
|
|
||
|
foreach ( $color_matches[0] as $color_match ) {
|
||
|
$color_pieces = [];
|
||
|
|
||
|
// Clean up the color from each of the matches above.
|
||
|
preg_match( '/^rgba?\([^)]*\)$/', $color_match, $color_pieces );
|
||
|
|
||
|
// Remove the whole `rgb(*)` / `rgba(*) bit that was matched above from the CSS.
|
||
|
$css_test_string = str_replace( $color_match, '', $css_test_string );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( $found && $transform_attr ) {
|
||
|
$css_value = trim( $parts[1] );
|
||
|
if ( preg_match( '/^((matrix|matrix3d|perspective|rotate|rotate3d|rotateX|rotateY|rotateZ|translate|translate3d|translateX|translatY|translatZ|scale|scale3d|scalX|scaleY|scaleZ|skew|skewX|skeY)\(([^()])*\) ?)+$/', $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 calc().
|
||
|
$css_test_string = (string) preg_replace( '/calc\(((?:\([^()]*\)?|[^()])*)\)/', '', $css_test_string );
|
||
|
// Allow CSS var().
|
||
|
$css_test_string = (string) preg_replace( '/\(?var\(--[a-zA-Z0-9_-]*\)/', '', $css_test_string );
|
||
|
|
||
|
// Check for any CSS containing \ ( & } = or comments,
|
||
|
// except for url(), calc(), or var() usage checked above.
|
||
|
$allow_css = ! preg_match( '%[\\\(&=}]|/\*%', $css_test_string );
|
||
|
|
||
|
/** This filter is documented in wp-includes/kses.php */
|
||
|
$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;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Filter the allowed tags for KSES to allow for complete amp-story document markup.
|
||
|
*
|
||
|
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
|
||
|
*
|
||
|
* @since 1.0.0
|
||
|
*
|
||
|
* @param array<string, array<string,bool>>|mixed $allowed_tags Allowed tags.
|
||
|
* @return array<string, array<string,bool>>|mixed Allowed tags.
|
||
|
*
|
||
|
* @template T
|
||
|
*
|
||
|
* @phpstan-return ($allowed_tags is array<T> ? array<T> : mixed)
|
||
|
*/
|
||
|
public function filter_kses_allowed_html( $allowed_tags ) {
|
||
|
if ( ! \is_array( $allowed_tags ) ) {
|
||
|
return $allowed_tags;
|
||
|
}
|
||
|
|
||
|
$story_components = [
|
||
|
'html' => [
|
||
|
'amp' => true,
|
||
|
'lang' => true,
|
||
|
],
|
||
|
'head' => [],
|
||
|
'body' => [],
|
||
|
'meta' => [
|
||
|
'name' => true,
|
||
|
'content' => true,
|
||
|
'charset' => true,
|
||
|
],
|
||
|
'script' => [
|
||
|
'async' => true,
|
||
|
'src' => true,
|
||
|
'custom-element' => true,
|
||
|
'type' => true,
|
||
|
],
|
||
|
'noscript' => [],
|
||
|
'link' => [
|
||
|
'href' => true,
|
||
|
'rel' => true,
|
||
|
],
|
||
|
'style' => [
|
||
|
'type' => true,
|
||
|
'amp-boilerplate' => true,
|
||
|
'amp-custom' => true,
|
||
|
],
|
||
|
'amp-story' => [
|
||
|
'background-audio' => true,
|
||
|
'live-story' => true,
|
||
|
'live-story-disabled' => true,
|
||
|
'poster-landscape-src' => true,
|
||
|
'poster-portrait-src' => true,
|
||
|
'poster-square-src' => true,
|
||
|
'publisher' => true,
|
||
|
'publisher-logo-src' => true,
|
||
|
'standalone' => true,
|
||
|
'supports-landscape' => true,
|
||
|
'title' => true,
|
||
|
],
|
||
|
'amp-story-captions' => [
|
||
|
'height' => true,
|
||
|
'style-preset' => true,
|
||
|
],
|
||
|
'amp-story-shopping-attachment' => [
|
||
|
'cta-text' => true,
|
||
|
'theme' => true,
|
||
|
'src' => true,
|
||
|
],
|
||
|
'amp-story-shopping-config' => [
|
||
|
'src' => true,
|
||
|
],
|
||
|
'amp-story-shopping-tag' => [],
|
||
|
'amp-story-page' => [
|
||
|
'auto-advance-after' => true,
|
||
|
'background-audio' => true,
|
||
|
'id' => true,
|
||
|
],
|
||
|
'amp-story-page-attachment' => [
|
||
|
'href' => true,
|
||
|
'theme' => true,
|
||
|
],
|
||
|
'amp-story-page-outlink' => [
|
||
|
'cta-image' => true,
|
||
|
'theme' => true,
|
||
|
'cta-accent-color' => true,
|
||
|
'cta-accent-element' => true,
|
||
|
],
|
||
|
'amp-story-grid-layer' => [
|
||
|
'aspect-ratio' => true,
|
||
|
'position' => true,
|
||
|
'template' => true,
|
||
|
],
|
||
|
'amp-story-cta-layer' => [],
|
||
|
'amp-story-animation' => [
|
||
|
'trigger' => true,
|
||
|
],
|
||
|
'amp-img' => [
|
||
|
'alt' => true,
|
||
|
'attribution' => true,
|
||
|
'data-amp-bind-alt' => true,
|
||
|
'data-amp-bind-attribution' => true,
|
||
|
'data-amp-bind-src' => true,
|
||
|
'data-amp-bind-srcset' => true,
|
||
|
'disable-inline-width' => true,
|
||
|
'lightbox' => true,
|
||
|
'lightbox-thumbnail-id' => true,
|
||
|
'media' => true,
|
||
|
'noloading' => true,
|
||
|
'object-fit' => true,
|
||
|
'object-position' => true,
|
||
|
'placeholder' => true,
|
||
|
'sizes' => true,
|
||
|
'src' => true,
|
||
|
'srcset' => true,
|
||
|
],
|
||
|
'amp-video' => [
|
||
|
'album' => true,
|
||
|
'alt' => true,
|
||
|
'artist' => true,
|
||
|
'artwork' => true,
|
||
|
'attribution' => true,
|
||
|
'autoplay' => true,
|
||
|
'captions-id' => true,
|
||
|
'controls' => true,
|
||
|
'controlslist' => true,
|
||
|
'crossorigin' => true,
|
||
|
'data-amp-bind-album' => true,
|
||
|
'data-amp-bind-alt' => true,
|
||
|
'data-amp-bind-artist' => true,
|
||
|
'data-amp-bind-artwork' => true,
|
||
|
'data-amp-bind-attribution' => true,
|
||
|
'data-amp-bind-controls' => true,
|
||
|
'data-amp-bind-controlslist' => true,
|
||
|
'data-amp-bind-loop' => true,
|
||
|
'data-amp-bind-poster' => true,
|
||
|
'data-amp-bind-preload' => true,
|
||
|
'data-amp-bind-src' => true,
|
||
|
'data-amp-bind-title' => true,
|
||
|
'disableremoteplayback' => true,
|
||
|
'dock' => true,
|
||
|
'lightbox' => true,
|
||
|
'lightbox-thumbnail-id' => true,
|
||
|
'loop' => true,
|
||
|
'media' => true,
|
||
|
'muted' => true,
|
||
|
'noaudio' => true,
|
||
|
'noloading' => true,
|
||
|
'object-fit' => true,
|
||
|
'object-position' => true,
|
||
|
'placeholder' => true,
|
||
|
'poster' => true,
|
||
|
'preload' => true,
|
||
|
'rotate-to-fullscreen' => true,
|
||
|
'src' => true,
|
||
|
],
|
||
|
'source' => [
|
||
|
'type' => true,
|
||
|
'src' => true,
|
||
|
],
|
||
|
'img' => [
|
||
|
'alt' => true,
|
||
|
'attribution' => true,
|
||
|
'border' => true,
|
||
|
'decoding' => true,
|
||
|
'height' => true,
|
||
|
'importance' => true,
|
||
|
'intrinsicsize' => true,
|
||
|
'ismap' => true,
|
||
|
'loading' => true,
|
||
|
'longdesc' => true,
|
||
|
'sizes' => true,
|
||
|
'src' => true,
|
||
|
'srcset' => true,
|
||
|
'srcwidth' => true,
|
||
|
'width' => true,
|
||
|
],
|
||
|
'svg' => [
|
||
|
'width' => true,
|
||
|
'height' => true,
|
||
|
'viewbox' => true,
|
||
|
'fill' => true,
|
||
|
'xmlns' => true,
|
||
|
],
|
||
|
'clippath' => [
|
||
|
'transform' => true,
|
||
|
'clippathunits' => true,
|
||
|
'path' => true,
|
||
|
],
|
||
|
'defs' => [],
|
||
|
'feblend' => [
|
||
|
'in' => true,
|
||
|
'in2' => true,
|
||
|
'result' => true,
|
||
|
],
|
||
|
'fecolormatrix' => [
|
||
|
'in' => true,
|
||
|
'values' => true,
|
||
|
],
|
||
|
'feflood' => [
|
||
|
'flood-opacity' => true,
|
||
|
'result' => true,
|
||
|
],
|
||
|
'fegaussianblur' => [
|
||
|
'stddeviation' => true,
|
||
|
],
|
||
|
'feoffset' => [],
|
||
|
'filter' => [
|
||
|
'id' => true,
|
||
|
'x' => true,
|
||
|
'y' => true,
|
||
|
'width' => true,
|
||
|
'height' => true,
|
||
|
'filterunits' => true,
|
||
|
'color-interpolation-filters' => true,
|
||
|
],
|
||
|
'g' => [
|
||
|
'filter' => true,
|
||
|
'opacity' => true,
|
||
|
],
|
||
|
'path' => [
|
||
|
'd' => true,
|
||
|
'fill-rule' => true,
|
||
|
'clip-rule' => true,
|
||
|
'fill' => true,
|
||
|
],
|
||
|
];
|
||
|
|
||
|
$allowed_tags = $this->array_merge_recursive_distinct( $allowed_tags, $story_components );
|
||
|
|
||
|
$allowed_tags = array_map( [ $this, 'add_global_attributes' ], $allowed_tags );
|
||
|
|
||
|
return $allowed_tags;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Temporarily renames the style attribute to data-temp-style in full story markup.
|
||
|
*
|
||
|
* @since 1.0.0
|
||
|
*
|
||
|
* @param string $post_content Post content.
|
||
|
* @return string Filtered post content.
|
||
|
*/
|
||
|
public function filter_content_save_pre_before_kses( string $post_content ): string {
|
||
|
return (string) preg_replace_callback(
|
||
|
'|(?P<before><\w+(?:-\w+)*\s[^>]*?)style=\\\"(?P<styles>[^"]*)\\\"(?P<after>([^>]+?)*>)|', // Extra slashes appear here because $post_content is pre-slashed..
|
||
|
static fn( $matches ) => $matches['before'] . sprintf( ' data-temp-style="%s" ', $matches['styles'] ) . $matches['after'],
|
||
|
$post_content
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Renames data-temp-style back to style in full story markup.
|
||
|
*
|
||
|
* @since 1.0.0
|
||
|
*
|
||
|
* @param string $post_content Post content.
|
||
|
* @return string Filtered post content.
|
||
|
*/
|
||
|
public function filter_content_save_pre_after_kses( string $post_content ): string {
|
||
|
return (string) preg_replace_callback(
|
||
|
'/ data-temp-style=\\\"(?P<styles>[^"]*)\\\"/',
|
||
|
function ( $matches ) {
|
||
|
$styles = str_replace( '"', '\"', $matches['styles'] );
|
||
|
return sprintf( ' style="%s"', esc_attr( $this->safecss_filter_attr( wp_kses_stripslashes( $styles ) ) ) );
|
||
|
},
|
||
|
$post_content
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Checks whether the post type is correct and user has capability to edit it.
|
||
|
*
|
||
|
* @since 1.22.0
|
||
|
*
|
||
|
* @param string $post_type Post type slug.
|
||
|
* @param int|string|null $post_parent Parent post ID.
|
||
|
* @return bool Whether the user can edit the provided post type.
|
||
|
*/
|
||
|
private function is_allowed_post_type( string $post_type, $post_parent ): bool {
|
||
|
if ( $this->story_post_type->get_slug() === $post_type && $this->story_post_type->has_cap( 'edit_posts' ) ) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
if ( $this->page_template_post_type->get_slug() === $post_type && $this->page_template_post_type->has_cap( 'edit_posts' ) ) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// For story autosaves.
|
||
|
if (
|
||
|
(
|
||
|
'revision' === $post_type &&
|
||
|
! empty( $post_parent ) &&
|
||
|
get_post_type( (int) $post_parent ) === $this->story_post_type->get_slug()
|
||
|
) &&
|
||
|
$this->story_post_type->has_cap( 'edit_posts' )
|
||
|
) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Filters story data.
|
||
|
*
|
||
|
* Provides simple sanity check to ensure story data is valid JSON.
|
||
|
*
|
||
|
* @since 1.22.0
|
||
|
*
|
||
|
* @param string $story_data JSON-encoded story data.
|
||
|
* @return string Sanitized & slashed story data.
|
||
|
*/
|
||
|
private function filter_story_data( string $story_data ): string {
|
||
|
$decoded = json_decode( (string) wp_unslash( $story_data ), true );
|
||
|
return null === $decoded ? '' : wp_slash( (string) wp_json_encode( $decoded ) );
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Recursively merge multiple arrays and ensure values are distinct.
|
||
|
*
|
||
|
* Based on information found in http://www.php.net/manual/en/function.array-merge-recursive.php
|
||
|
*
|
||
|
* @since 1.5.0
|
||
|
*
|
||
|
* @param array<int|string,mixed> ...$arrays [optional] Variable list of arrays to recursively merge.
|
||
|
* @return array<int|string,mixed> An array of values resulted from merging the arguments together.
|
||
|
*/
|
||
|
protected function array_merge_recursive_distinct( array ...$arrays ): array {
|
||
|
if ( \count( $arrays ) < 2 ) {
|
||
|
if ( [] === $arrays ) {
|
||
|
return $arrays;
|
||
|
}
|
||
|
|
||
|
return array_shift( $arrays );
|
||
|
}
|
||
|
|
||
|
$merged = array_shift( $arrays );
|
||
|
|
||
|
foreach ( $arrays as $array ) {
|
||
|
foreach ( $array as $key => $value ) {
|
||
|
if ( \is_array( $value ) && ( isset( $merged[ $key ] ) && \is_array( $merged[ $key ] ) ) ) {
|
||
|
$merged[ $key ] = $this->array_merge_recursive_distinct( $merged[ $key ], $value );
|
||
|
} else {
|
||
|
$merged[ $key ] = $value;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $merged;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper function to add global attributes to a tag in the allowed HTML list.
|
||
|
*
|
||
|
* @since 1.0.0
|
||
|
*
|
||
|
* @see _wp_add_global_attributes
|
||
|
*
|
||
|
* @param array<string,bool> $value An array of attributes.
|
||
|
* @return array<string,bool> The array of attributes with global attributes added.
|
||
|
*/
|
||
|
protected function add_global_attributes( array $value ): array {
|
||
|
$global_attributes = [
|
||
|
'aria-describedby' => true,
|
||
|
'aria-details' => true,
|
||
|
'aria-label' => true,
|
||
|
'aria-labelledby' => true,
|
||
|
'aria-hidden' => true,
|
||
|
'class' => true,
|
||
|
'id' => true,
|
||
|
'style' => true,
|
||
|
'title' => true,
|
||
|
'role' => true,
|
||
|
'data-*' => true,
|
||
|
'animate-in' => true,
|
||
|
'animate-in-duration' => true,
|
||
|
'animate-in-delay' => true,
|
||
|
'animate-in-after' => true,
|
||
|
'animate-in-layout' => true,
|
||
|
'layout' => true,
|
||
|
];
|
||
|
|
||
|
return array_merge( $value, $global_attributes );
|
||
|
}
|
||
|
}
|