HEX
Server: nginx/1.27.1
System: Linux in-3 5.15.0-161-generic #171-Ubuntu SMP Sat Oct 11 08:17:01 UTC 2025 x86_64
User: ivenus-clone (3297)
PHP: 7.4.33
Disabled: exec,passthru,shell_exec,system,proc_open,popen,parse_ini_file,show_source
Upload Files
File: /storage/v4513/ivenusnew/public_html/wp-content/plugins/web-stories/includes/KSES.php
<?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\AMP\Meta_Sanitizer;
use Google\Web_Stories\AMP\Tag_And_Attribute_Sanitizer;
use Google\Web_Stories\Infrastructure\HasRequirements;
use Google\Web_Stories_Dependencies\AMP_Content_Sanitizer;
use Google\Web_Stories_Dependencies\AMP_Script_Sanitizer;
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;

/**
 * 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 full AMP sanitization instead of just KSES.
	 *
	 * This allows storing full, valid AMP HTML documents in post_content for stories, which require
	 * more allowed HTML tags.
	 *
	 * @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|mixed $data
	 * @phpstan-param PostData|mixed $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 (
			! \is_string( $data['post_type'] ) ||
			! $this->is_allowed_post_type( $data['post_type'], $data['post_parent'] )
		) {
			return $data;
		}

		if (
			isset( $unsanitized_postarr['post_content_filtered'] ) &&
			\is_string( $unsanitized_postarr['post_content_filtered'] )
		) {
			$data['post_content_filtered'] = $this->filter_story_data( $unsanitized_postarr['post_content_filtered'] );
		}

		if ( isset( $unsanitized_postarr['post_content'] ) ) {
			$data['post_content'] = wp_slash( $this->sanitize_content( wp_unslash( $unsanitized_postarr['post_content'] ) ) );
		}

		return $data;
	}

	/**
	 * Sanitizes post content.
	 *
	 * @since 1.37.0
	 *
	 * @param string $content Unsanitized post content.
	 */
	private function sanitize_content( string $content ): string {
		$dom = Document::fromHtml( $content );
		if ( $dom instanceof Document ) {
			$sanitizers = $this->get_sanitizers();
			AMP_Content_Sanitizer::sanitize_document( $dom, $sanitizers, [] );
			return trim( $dom->saveHTML() );
		}

		return '';
	}

	/**
	 * Returns a list of sanitizers to use.
	 *
	 * This is replica of the Sanitization class implementation
	 * to have a minimal AMP sanitization for user-provided input.
	 *
	 * @since 1.37.0
	 *
	 * @see Sanitization
	 *
	 * @return array<string,array<string,bool|string[]|string>> Sanitizers.
	 */
	private function get_sanitizers(): array {
		$sanitizers = [
			AMP_Script_Sanitizer::class        => [
				'sanitize_js_scripts' => true,
			],
			Meta_Sanitizer::class              => [],
			Tag_And_Attribute_Sanitizer::class => [],
		];

		foreach ( $sanitizers as &$sanitizer ) {
			$sanitizer['validation_error_callback'] = static fn( array $error, array $data = [] ): bool => apply_filters( 'web_stories_amp_validation_error_sanitized', true, $error ); // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
		}

		unset( $sanitizer );

		return $sanitizers;
	}

	/**
	 * 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 ) );
	}
}