Commit realizado el 12:13:52 08-04-2024
This commit is contained in:
19
wp-content/plugins/web-stories/third-party/vendor/ampproject/amp-wp/includes/bootstrap.php
vendored
Normal file
19
wp-content/plugins/web-stories/third-party/vendor/ampproject/amp-wp/includes/bootstrap.php
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Google\Web_Stories_Dependencies;
|
||||
|
||||
/**
|
||||
* Central bootstrapping entry point for all non-autoloaded files.
|
||||
*
|
||||
* This file is mainly used for taking direct control of included files off from Composer's
|
||||
* "files" directive, as that one can easily include the files multiple times, leading to
|
||||
* redeclaration fatal errors.
|
||||
*
|
||||
* @package AmpProject/AmpWP
|
||||
*/
|
||||
$files_to_include = [__DIR__ . '/../back-compat/back-compat.php' => 'amp_backcompat_use_v03_templates', __DIR__ . '/../includes/amp-helper-functions.php' => 'amp_activate', __DIR__ . '/../includes/admin/functions.php' => 'amp_init_customizer', __DIR__ . '/../includes/deprecated.php' => 'amp_load_classes'];
|
||||
foreach ($files_to_include as $file_to_include => $function_to_check) {
|
||||
if (!\function_exists($function_to_check)) {
|
||||
include $file_to_include;
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,844 @@
|
||||
<?php
|
||||
|
||||
namespace Google\Web_Stories_Dependencies;
|
||||
|
||||
/**
|
||||
* Class AMP_Base_Sanitizer
|
||||
*
|
||||
* @package AMP
|
||||
*/
|
||||
use Google\Web_Stories_Dependencies\AmpProject\AmpWP\ValidationExemption;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\CssLength;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\DevMode;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
|
||||
/**
|
||||
* Class AMP_Base_Sanitizer
|
||||
*
|
||||
* @since 0.2
|
||||
*/
|
||||
abstract class AMP_Base_Sanitizer
|
||||
{
|
||||
/**
|
||||
* Value used with the height attribute in an $attributes parameter is empty.
|
||||
*
|
||||
* @since 0.3.3
|
||||
*
|
||||
* @const int
|
||||
*/
|
||||
const FALLBACK_HEIGHT = 400;
|
||||
/**
|
||||
* Placeholder for default args, to be set in child classes.
|
||||
*
|
||||
* @since 0.2
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $DEFAULT_ARGS = [];
|
||||
/**
|
||||
* DOM.
|
||||
*
|
||||
* @var Document An AmpProject\Document representation of an HTML document.
|
||||
*
|
||||
* @since 0.2
|
||||
*/
|
||||
protected $dom;
|
||||
/**
|
||||
* Array of flags used to control sanitization.
|
||||
*
|
||||
* @var array {
|
||||
* @type int $content_max_width
|
||||
* @type bool $add_placeholder
|
||||
* @type bool $use_document_element
|
||||
* @type bool $require_https_src
|
||||
* @type string[] $amp_allowed_tags
|
||||
* @type string[] $amp_globally_allowed_attributes
|
||||
* @type string[] $amp_layout_allowed_attributes
|
||||
* @type array $amp_allowed_tags
|
||||
* @type array $amp_globally_allowed_attributes
|
||||
* @type array $amp_layout_allowed_attributes
|
||||
* @type array $amp_bind_placeholder_prefix
|
||||
* @type bool $should_locate_sources
|
||||
* @type callable $validation_error_callback
|
||||
* }
|
||||
*/
|
||||
protected $args;
|
||||
/**
|
||||
* Flag to be set in child class' sanitize() method indicating if the
|
||||
* HTML contained in the Dom\Document has been sanitized yet or not.
|
||||
*
|
||||
* @since 0.2
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $did_convert_elements = \false;
|
||||
/**
|
||||
* The root element used for sanitization. Either html or body.
|
||||
*
|
||||
* @var DOMElement
|
||||
*/
|
||||
protected $root_element;
|
||||
/**
|
||||
* AMP_Base_Sanitizer constructor.
|
||||
*
|
||||
* @since 0.2
|
||||
*
|
||||
* @param Document $dom Represents the HTML document to sanitize.
|
||||
* @param array $args {
|
||||
* Args.
|
||||
*
|
||||
* @type int $content_max_width
|
||||
* @type bool $add_placeholder
|
||||
* @type bool $require_https_src
|
||||
* @type string[] $amp_allowed_tags
|
||||
* @type string[] $amp_globally_allowed_attributes
|
||||
* @type string[] $amp_layout_allowed_attributes
|
||||
* }
|
||||
*/
|
||||
public function __construct($dom, $args = [])
|
||||
{
|
||||
$this->dom = $dom;
|
||||
$this->args = \array_merge($this->DEFAULT_ARGS, $args);
|
||||
if (!empty($this->args['use_document_element'])) {
|
||||
$this->root_element = $this->dom->documentElement;
|
||||
} else {
|
||||
$this->root_element = $this->dom->body;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Add filters to manipulate output during output buffering before the DOM is constructed.
|
||||
*
|
||||
* Add actions and filters before the page is rendered so that the sanitizer can fix issues during output buffering.
|
||||
* This provides an alternative to manipulating the DOM in the sanitize method. This is a static function because
|
||||
* it is invoked before the class is instantiated, as the DOM is not available yet. This method is only called
|
||||
* when 'amp' theme support is present. It is conceptually similar to the AMP_Base_Embed_Handler class's register_embed
|
||||
* method.
|
||||
*
|
||||
* @since 1.0
|
||||
* @see \AMP_Base_Embed_Handler::register_embed()
|
||||
*
|
||||
* @param array $args Args.
|
||||
*/
|
||||
public static function add_buffering_hooks($args = [])
|
||||
{
|
||||
}
|
||||
// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
/**
|
||||
* Get mapping of HTML selectors to the AMP component selectors which they may be converted into.
|
||||
*
|
||||
* @see AMP_Style_Sanitizer::ampify_ruleset_selectors()
|
||||
*
|
||||
* @return array Mapping.
|
||||
*/
|
||||
public function get_selector_conversion_mapping()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
/**
|
||||
* Determine whether the resulting AMP element uses a "light" shadow DOM.
|
||||
*
|
||||
* Sometimes AMP components serve as wrappers for native elements, like `amp-img` for `img`. When this
|
||||
* is the case, authors sometimes will want to style the shadow element (such as to set object-fit). Normally if a
|
||||
* selector contains `img` then the style sanitizer will always convert this to `amp-img` (and `amp-anim`), which
|
||||
* may break the author's intended selector target. So when using a sanitizer's selector conversion mapping to
|
||||
* rewrite non-AMP to AMP selectors, it will first check to see if the selector already mentions an AMP tag and if
|
||||
* so it will skip the conversions for that selector. In this way, an `amp-img img` selector will not get converted
|
||||
* into `amp-img amp-img`. The selector mapping also is involved when doing tree shaking. In the case of the
|
||||
* selector `amp-img img`, the tree shaker would normally strip out this selector because no `img` may be present
|
||||
* in the page as it is added by the AMP runtime (unless noscript fallbacks have been added, and this also
|
||||
* disregards data-hero images which are added later by AMP Optimizer). So in order to prevent such selectors from
|
||||
* being stripped out, it's important to include the `amp-img` selector among the `dynamic_element_selectors` so
|
||||
* that the `img` in the `amp-img img` selector is ignored for the purposes of tree shaking. This method is used
|
||||
* to indicate which sanitizers are involved in such element conversions. If this method returns true, then the
|
||||
* keys in the selector conversion mapping should be used as `dynamic_element_selectors`.
|
||||
*
|
||||
* In other words, this method indicates whether keys in the conversion mapping are ancestors of elements which are
|
||||
* created at runtime. This method is only relevant when the `get_selector_conversion_mapping()` method returns a
|
||||
* mapping.
|
||||
*
|
||||
* @since 2.2
|
||||
* @see AMP_Style_Sanitizer::ampify_ruleset_selectors()
|
||||
* @see AMP_Base_Sanitizer::get_selector_conversion_mapping()
|
||||
*
|
||||
* @return bool Whether light DOM is used.
|
||||
*/
|
||||
public function has_light_shadow_dom()
|
||||
{
|
||||
return \true;
|
||||
}
|
||||
/**
|
||||
* Get arg.
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param string $key Arg key.
|
||||
* @return mixed Args.
|
||||
*/
|
||||
public function get_arg($key)
|
||||
{
|
||||
if (\array_key_exists($key, $this->args)) {
|
||||
return $this->args[$key];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Get args.
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @return array Args.
|
||||
*/
|
||||
public function get_args()
|
||||
{
|
||||
return $this->args;
|
||||
}
|
||||
/**
|
||||
* Update args.
|
||||
*
|
||||
* Merges the supplied args with the existing args.
|
||||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param array $args Args.
|
||||
*/
|
||||
public function update_args($args)
|
||||
{
|
||||
$this->args = \array_merge($this->args, $args);
|
||||
}
|
||||
/**
|
||||
* Run logic before any sanitizers are run.
|
||||
*
|
||||
* After the sanitizers are instantiated but before calling sanitize on each of them, this
|
||||
* method is called with list of all the instantiated sanitizers.
|
||||
*
|
||||
* @param AMP_Base_Sanitizer[] $sanitizers Sanitizers.
|
||||
*/
|
||||
public function init($sanitizers)
|
||||
{
|
||||
}
|
||||
// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
/**
|
||||
* Sanitize the HTML contained in the DOMDocument received by the constructor
|
||||
*/
|
||||
public abstract function sanitize();
|
||||
/**
|
||||
* Return array of values that would be valid as an HTML `script` element.
|
||||
*
|
||||
* Array keys are AMP element names and array values are their respective
|
||||
* Javascript URLs from https://cdn.ampproject.org
|
||||
*
|
||||
* @since 0.2
|
||||
*
|
||||
* @return string[] Returns component name as array key and JavaScript URL as array value,
|
||||
* respectively. Will return an empty array if sanitization has yet to be run
|
||||
* or if it did not find any HTML elements to convert to AMP equivalents.
|
||||
*/
|
||||
public function get_scripts()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
/**
|
||||
* Return array of values that would be valid as an HTML `style` attribute.
|
||||
*
|
||||
* @since 0.4
|
||||
* @codeCoverageIgnore
|
||||
* @deprecated As of 1.0, use get_stylesheets().
|
||||
* @internal
|
||||
*
|
||||
* @return array[][] Mapping of CSS selectors to arrays of properties.
|
||||
*/
|
||||
public function get_styles()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
/**
|
||||
* Get stylesheets.
|
||||
*
|
||||
* @since 0.7
|
||||
* @return array Values are the CSS stylesheets. Keys are MD5 hashes of the stylesheets.
|
||||
*/
|
||||
public function get_stylesheets()
|
||||
{
|
||||
$stylesheets = [];
|
||||
foreach ($this->get_styles() as $selector => $properties) {
|
||||
$stylesheet = \sprintf('%s { %s }', $selector, \implode('; ', $properties) . ';');
|
||||
$stylesheets[\md5($stylesheet)] = $stylesheet;
|
||||
}
|
||||
return $stylesheets;
|
||||
}
|
||||
/**
|
||||
* Get HTML body as DOMElement from Dom\Document received by the constructor.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
* @deprecated Use $this->dom->body instead.
|
||||
* @return DOMElement The body element.
|
||||
*/
|
||||
protected function get_body_node()
|
||||
{
|
||||
\_deprecated_function('Use $this->dom->body instead', '1.5.0');
|
||||
return $this->dom->body;
|
||||
}
|
||||
/**
|
||||
* Sanitizes a CSS dimension specifier while being sensitive to dimension context.
|
||||
*
|
||||
* @param string $value A valid CSS dimension specifier; e.g. 50, 50px, 50%. Can be 'auto' for width.
|
||||
* @param string $dimension Dimension, either 'width' or 'height'.
|
||||
*
|
||||
* @return float|int|string Returns a numeric dimension value, 'auto', or an empty string.
|
||||
*/
|
||||
public function sanitize_dimension($value, $dimension)
|
||||
{
|
||||
// Allows 0 to be used as valid dimension.
|
||||
if (empty($value) && '0' !== (string) $value) {
|
||||
return '';
|
||||
}
|
||||
// Accepts both integers and floats & prevents negative values.
|
||||
if (\is_numeric($value)) {
|
||||
return \max(0, (float) $value);
|
||||
}
|
||||
if (\Google\Web_Stories_Dependencies\AMP_String_Utils::endswith($value, '%') && 'width' === $dimension) {
|
||||
if ('100%' === $value) {
|
||||
return 'auto';
|
||||
} elseif (isset($this->args['content_max_width'])) {
|
||||
$percentage = \absint($value) / 100;
|
||||
return \round($percentage * $this->args['content_max_width']);
|
||||
}
|
||||
}
|
||||
$length = new CssLength($value);
|
||||
$length->validate('width' === $dimension, \false);
|
||||
if ($length->isValid()) {
|
||||
if ($length->isAuto()) {
|
||||
return 'auto';
|
||||
}
|
||||
return $length->getNumeral() . ($length->getUnit() === 'px' ? '' : $length->getUnit());
|
||||
}
|
||||
return '';
|
||||
}
|
||||
/**
|
||||
* Determine if an attribute value is empty.
|
||||
*
|
||||
* @param string|null $value Attribute value.
|
||||
* @return bool True if empty, false if not.
|
||||
*/
|
||||
public function is_empty_attribute_value($value)
|
||||
{
|
||||
return !isset($value) || '' === $value;
|
||||
}
|
||||
/**
|
||||
* Sets the layout, and possibly the 'height' and 'width' attributes.
|
||||
*
|
||||
* @param array $attributes {
|
||||
* Attributes.
|
||||
*
|
||||
* @type string $bottom
|
||||
* @type int|string $height
|
||||
* @type string $layout
|
||||
* @type string $left
|
||||
* @type string $position
|
||||
* @type string $right
|
||||
* @type string $style
|
||||
* @type string $top
|
||||
* @type int|string $width
|
||||
* }
|
||||
* @return array Attributes.
|
||||
*/
|
||||
public function set_layout($attributes)
|
||||
{
|
||||
if (isset($attributes['layout']) && ('fill' === $attributes['layout'] || 'flex-item' !== $attributes['layout'])) {
|
||||
return $attributes;
|
||||
}
|
||||
// Special-case handling for inline style that should be transformed into layout=fill.
|
||||
if (!empty($attributes['style'])) {
|
||||
$styles = $this->parse_style_string($attributes['style']);
|
||||
// Apply fill layout if top, left, bottom, right are used.
|
||||
if (isset($styles['position'], $styles['top'], $styles['left'], $styles['bottom'], $styles['right']) && 'absolute' === $styles['position'] && 0 === (int) $styles['top'] && 0 === (int) $styles['left'] && 0 === (int) $styles['bottom'] && 0 === (int) $styles['right'] && (!isset($attributes['width']) || '100%' === $attributes['width']) && (!isset($attributes['height']) || '100%' === $attributes['height'])) {
|
||||
unset($attributes['style'], $styles['position'], $styles['top'], $styles['left'], $styles['bottom'], $styles['right']);
|
||||
if (!empty($styles)) {
|
||||
$attributes['style'] = $this->reassemble_style_string($styles);
|
||||
}
|
||||
$attributes['layout'] = 'fill';
|
||||
unset($attributes['height'], $attributes['width']);
|
||||
return $attributes;
|
||||
}
|
||||
// Apply fill layout if top, left, width, height are used.
|
||||
if (isset($styles['position'], $styles['top'], $styles['left'], $styles['width'], $styles['height']) && 'absolute' === $styles['position'] && 0 === (int) $styles['top'] && 0 === (int) $styles['left'] && '100%' === (string) $styles['width'] && '100%' === (string) $styles['height']) {
|
||||
unset($attributes['style'], $styles['position'], $styles['top'], $styles['left'], $styles['width'], $styles['height']);
|
||||
if (!empty($styles)) {
|
||||
$attributes['style'] = $this->reassemble_style_string($styles);
|
||||
}
|
||||
$attributes['layout'] = 'fill';
|
||||
unset($attributes['height'], $attributes['width']);
|
||||
return $attributes;
|
||||
}
|
||||
// Apply fill layout if width & height are 100%.
|
||||
if (isset($styles['position']) && 'absolute' === $styles['position'] && (isset($attributes['width']) && '100%' === $attributes['width'] || isset($styles['width']) && '100%' === $styles['width']) && (isset($attributes['height']) && '100%' === $attributes['height'] || isset($styles['height']) && '100%' === $styles['height'])) {
|
||||
unset($attributes['style'], $attributes['width'], $attributes['height']);
|
||||
unset($styles['position'], $styles['width'], $styles['height']);
|
||||
if (!empty($styles)) {
|
||||
$attributes['style'] = $this->reassemble_style_string($styles);
|
||||
}
|
||||
$attributes['layout'] = 'fill';
|
||||
return $attributes;
|
||||
}
|
||||
// Make sure the width and height styles are copied to the width and height attributes since AMP will ultimately inline them as styles.
|
||||
$pending_attributes = [];
|
||||
$seen_units = [];
|
||||
foreach (\wp_array_slice_assoc($styles, ['height', 'width']) as $dimension => $value) {
|
||||
$value = $this->sanitize_dimension($value, $dimension);
|
||||
if ('' === $value) {
|
||||
continue;
|
||||
}
|
||||
if ('auto' !== $value) {
|
||||
$this_unit = \preg_replace('/^.*\\d/', '', $value);
|
||||
if (!$this_unit) {
|
||||
$this_unit = 'px';
|
||||
}
|
||||
$seen_units[$this_unit] = \true;
|
||||
}
|
||||
$pending_attributes[$dimension] = $value;
|
||||
}
|
||||
if ($pending_attributes && \count($seen_units) <= 1) {
|
||||
$attributes = \array_merge($attributes, $pending_attributes);
|
||||
}
|
||||
}
|
||||
if (isset($attributes['width'], $attributes['height']) && '100%' === $attributes['width'] && '100%' === $attributes['height']) {
|
||||
unset($attributes['width'], $attributes['height']);
|
||||
$attributes['layout'] = 'fill';
|
||||
} else {
|
||||
if (empty($attributes['height'])) {
|
||||
unset($attributes['width']);
|
||||
$attributes['height'] = self::FALLBACK_HEIGHT;
|
||||
}
|
||||
if (empty($attributes['width']) || '100%' === $attributes['width'] || 'auto' === $attributes['width']) {
|
||||
$attributes['layout'] = 'fixed-height';
|
||||
$attributes['width'] = 'auto';
|
||||
}
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
/**
|
||||
* Adds or appends key and value to list of attributes
|
||||
*
|
||||
* Adds key and value to list of attributes, or if the key already exists in the array
|
||||
* it concatenates to existing attribute separator by a space or other supplied separator.
|
||||
*
|
||||
* @param string[] $attributes {
|
||||
* Attributes.
|
||||
*
|
||||
* @type int $height
|
||||
* @type int $width
|
||||
* @type string $sizes
|
||||
* @type string $class
|
||||
* @type string $layout
|
||||
* }
|
||||
* @param string $key Valid associative array index to add.
|
||||
* @param string $value Value to add or append to array indexed at the key.
|
||||
* @param string $separator Optional; defaults to space but some other separator if needed.
|
||||
*/
|
||||
public function add_or_append_attribute(&$attributes, $key, $value, $separator = ' ')
|
||||
{
|
||||
if (isset($attributes[$key])) {
|
||||
$attributes[$key] = \trim($attributes[$key] . $separator . $value);
|
||||
} else {
|
||||
$attributes[$key] = $value;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Decide if we should remove a src attribute if https is required.
|
||||
*
|
||||
* If not required, the implementing class may want to try and force https instead.
|
||||
*
|
||||
* @param string $src URL to convert to HTTPS if forced, or made empty if $args['require_https_src'].
|
||||
* @param boolean $force_https Force setting of HTTPS if true.
|
||||
* @return string URL which may have been updated with HTTPS, or may have been made empty.
|
||||
*/
|
||||
public function maybe_enforce_https_src($src, $force_https = \false)
|
||||
{
|
||||
$protocol = \strtok($src, ':');
|
||||
// @todo What about relative URLs? This should use wp_parse_url( $src, PHP_URL_SCHEME )
|
||||
if ('https' !== $protocol) {
|
||||
// Check if https is required.
|
||||
if (isset($this->args['require_https_src']) && \true === $this->args['require_https_src']) {
|
||||
// Remove the src. Let the implementing class decide what do from here.
|
||||
$src = '';
|
||||
} elseif ((!isset($this->args['require_https_src']) || \false === $this->args['require_https_src']) && \true === $force_https) {
|
||||
// Don't remove the src, but force https instead.
|
||||
$src = \set_url_scheme($src, 'https');
|
||||
}
|
||||
}
|
||||
return $src;
|
||||
}
|
||||
/**
|
||||
* Check whether the document of a given node is in dev mode.
|
||||
*
|
||||
* @since 1.3
|
||||
*
|
||||
* @deprecated Use AmpProject\DevMode::isActiveForDocument( $document ) instead.
|
||||
*
|
||||
* @return bool Whether the document is in dev mode.
|
||||
*/
|
||||
protected function is_document_in_dev_mode()
|
||||
{
|
||||
\_deprecated_function('AMP_Base_Sanitizer::is_document_in_dev_mode', '1.5', 'AmpProject\\DevMode::isActiveForDocument');
|
||||
return DevMode::isActiveForDocument($this->dom);
|
||||
}
|
||||
/**
|
||||
* Check whether a node is exempt from validation during dev mode.
|
||||
*
|
||||
* @since 1.3
|
||||
*
|
||||
* @deprecated Use AmpProject\DevMode::hasExemptionForNode( $node ) instead.
|
||||
*
|
||||
* @param DOMNode $node Node to check.
|
||||
* @return bool Whether the node should be exempt during dev mode.
|
||||
*/
|
||||
protected function has_dev_mode_exemption(\DOMNode $node)
|
||||
{
|
||||
\_deprecated_function('AMP_Base_Sanitizer::has_dev_mode_exemption', '1.5', 'AmpProject\\DevMode::hasExemptionForNode');
|
||||
return DevMode::hasExemptionForNode($node);
|
||||
}
|
||||
/**
|
||||
* Check whether a certain node should be exempt from validation.
|
||||
*
|
||||
* @deprecated Use AmpProject\DevMode::isExemptFromValidation( $node ) instead.
|
||||
*
|
||||
* @param DOMNode $node Node to check.
|
||||
* @return bool Whether the node should be exempt from validation.
|
||||
*/
|
||||
protected function is_exempt_from_validation(\DOMNode $node)
|
||||
{
|
||||
\_deprecated_function('AMP_Base_Sanitizer::is_exempt_from_validation', '1.5', 'AmpProject\\DevMode::isExemptFromValidation');
|
||||
return DevMode::isExemptFromValidation($node);
|
||||
}
|
||||
/**
|
||||
* Removes an invalid child of a node.
|
||||
*
|
||||
* Also, calls the mutation callback for it.
|
||||
* This tracks all the nodes that were removed.
|
||||
*
|
||||
* @since 0.7
|
||||
*
|
||||
* @param DOMNode|DOMElement $node The node to remove.
|
||||
* @param array $validation_error Validation error details.
|
||||
* @return bool Whether the node should have been removed, that is, that the node was sanitized for validity.
|
||||
*/
|
||||
public function remove_invalid_child($node, $validation_error = [])
|
||||
{
|
||||
if (DevMode::isExemptFromValidation($node)) {
|
||||
return \false;
|
||||
}
|
||||
if (ValidationExemption::is_amp_unvalidated_for_node($node) || ValidationExemption::is_px_verified_for_node($node)) {
|
||||
return \false;
|
||||
}
|
||||
$should_remove = $this->should_sanitize_validation_error($validation_error, \compact('node'));
|
||||
if ($should_remove) {
|
||||
if (null === $node->parentNode) {
|
||||
// Node no longer exists.
|
||||
return $should_remove;
|
||||
}
|
||||
$node->parentNode->removeChild($node);
|
||||
} else {
|
||||
ValidationExemption::mark_node_as_amp_unvalidated($node);
|
||||
}
|
||||
return $should_remove;
|
||||
}
|
||||
/**
|
||||
* Removes an invalid attribute of a node.
|
||||
*
|
||||
* Also, calls the mutation callback for it.
|
||||
* This tracks all the attributes that were removed.
|
||||
*
|
||||
* @since 0.7
|
||||
*
|
||||
* @param DOMElement $element The node for which to remove the attribute.
|
||||
* @param DOMAttr|string $attribute The attribute to remove from the element.
|
||||
* @param array $validation_error Validation error details.
|
||||
* @param array $attr_spec Attribute spec.
|
||||
* @return bool Whether the node should have been removed, that is, that the node was sanitized for validity.
|
||||
*/
|
||||
public function remove_invalid_attribute($element, $attribute, $validation_error = [], $attr_spec = [])
|
||||
{
|
||||
if (DevMode::isExemptFromValidation($element)) {
|
||||
return \false;
|
||||
}
|
||||
if (\is_string($attribute)) {
|
||||
$node = $element->getAttributeNode($attribute);
|
||||
} else {
|
||||
$node = $attribute;
|
||||
}
|
||||
// Catch edge condition (no known possible way to reach).
|
||||
if (!$node instanceof \DOMAttr || $element !== $node->parentNode) {
|
||||
return \false;
|
||||
}
|
||||
if (ValidationExemption::is_amp_unvalidated_for_node($node) || ValidationExemption::is_px_verified_for_node($node)) {
|
||||
return \false;
|
||||
}
|
||||
$should_remove = $this->should_sanitize_validation_error($validation_error, \compact('node'));
|
||||
if ($should_remove) {
|
||||
$allow_empty = !empty($attr_spec[\Google\Web_Stories_Dependencies\AMP_Rule_Spec::VALUE_URL][\Google\Web_Stories_Dependencies\AMP_Rule_Spec::ALLOW_EMPTY]);
|
||||
$is_href_attr = isset($attr_spec[\Google\Web_Stories_Dependencies\AMP_Rule_Spec::VALUE_URL]) && 'href' === $node->nodeName;
|
||||
if ($allow_empty && !$is_href_attr) {
|
||||
$node->nodeValue = '';
|
||||
} else {
|
||||
$element->removeAttributeNode($node);
|
||||
}
|
||||
} else {
|
||||
ValidationExemption::mark_node_as_amp_unvalidated($node);
|
||||
}
|
||||
return $should_remove;
|
||||
}
|
||||
/**
|
||||
* Check whether or not sanitization should occur in response to validation error.
|
||||
*
|
||||
* @since 1.0
|
||||
*
|
||||
* @param array $validation_error Validation error.
|
||||
* @param array $data Data including the node.
|
||||
* @return bool Whether to sanitize.
|
||||
*/
|
||||
public function should_sanitize_validation_error($validation_error, $data = [])
|
||||
{
|
||||
if (empty($this->args['validation_error_callback']) || !\is_callable($this->args['validation_error_callback'])) {
|
||||
return \true;
|
||||
}
|
||||
$validation_error = $this->prepare_validation_error($validation_error, $data);
|
||||
return \false !== \call_user_func($this->args['validation_error_callback'], $validation_error, $data);
|
||||
}
|
||||
/**
|
||||
* Prepare validation error.
|
||||
*
|
||||
* @param array $error {
|
||||
* Error.
|
||||
*
|
||||
* @type string $code Error code.
|
||||
* }
|
||||
* @param array $data {
|
||||
* Data.
|
||||
*
|
||||
* @type DOMElement|DOMNode $node The removed node.
|
||||
* }
|
||||
* @return array Error.
|
||||
*/
|
||||
public function prepare_validation_error(array $error = [], array $data = [])
|
||||
{
|
||||
$node = null;
|
||||
if (isset($data['node']) && $data['node'] instanceof \DOMNode) {
|
||||
$node = $data['node'];
|
||||
$error['node_name'] = $node->nodeName;
|
||||
if ($node->parentNode) {
|
||||
$error['parent_name'] = $node->parentNode->nodeName;
|
||||
}
|
||||
}
|
||||
if ($node instanceof \DOMElement) {
|
||||
if (!isset($error['code'])) {
|
||||
$error['code'] = \Google\Web_Stories_Dependencies\AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG;
|
||||
}
|
||||
if (!isset($error['type'])) {
|
||||
// @todo Also include javascript: protocol for URL errors.
|
||||
$error['type'] = 'script' === $node->nodeName ? \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::JS_ERROR_TYPE : \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE;
|
||||
}
|
||||
// @todo Change from node_attributes to element_attributes to harmonize the two.
|
||||
if (!isset($error['node_attributes'])) {
|
||||
$error['node_attributes'] = [];
|
||||
foreach ($node->attributes as $attribute) {
|
||||
$error['node_attributes'][$attribute->nodeName] = $attribute->nodeValue;
|
||||
}
|
||||
}
|
||||
// Capture element contents.
|
||||
$is_inline_script = 'script' === $node->nodeName && !$node->hasAttribute('src');
|
||||
$is_inline_style = 'style' === $node->nodeName && !$node->hasAttribute('amp-custom') && !$node->hasAttribute('amp-keyframes');
|
||||
if ($is_inline_script || $is_inline_style) {
|
||||
$text_content = $node->textContent;
|
||||
if ($is_inline_script) {
|
||||
// For inline scripts, normalize string and number literals to prevent nonces, random numbers, and timestamps
|
||||
// from generating endless number of validation errors.
|
||||
$error['text'] = \preg_replace([
|
||||
// Regex credit to <https://stackoverflow.com/a/5696141/93579>.
|
||||
'/"[^"\\\\\\n]*(?:\\\\.[^"\\\\\\n]*)*"/s',
|
||||
'/\'[^\'\\\\\\n]*(?:\\\\.[^\'\\\\\\n]*)*\'/s',
|
||||
'/(\\b|-)\\d+\\.\\d+\\b/',
|
||||
'/(\\b|-)\\d+\\b/',
|
||||
], ['__DOUBLE_QUOTED_STRING__', '__SINGLE_QUOTED_STRING__', '__FLOAT__', '__INT__'], $text_content);
|
||||
} else {
|
||||
// Include stylesheet text except for amp-custom and amp-keyframes since it is large and since it should
|
||||
// already be detailed in the stylesheets metabox.
|
||||
$error['text'] = $text_content;
|
||||
}
|
||||
}
|
||||
// Suppress 'ver' param from enqueued scripts and styles.
|
||||
if ('script' === $node->nodeName && isset($error['node_attributes']['src']) && \false !== \strpos($error['node_attributes']['src'], 'ver=')) {
|
||||
$error['node_attributes']['src'] = \add_query_arg('ver', '__normalized__', $error['node_attributes']['src']);
|
||||
} elseif ('link' === $node->nodeName && isset($error['node_attributes']['href']) && \false !== \strpos($error['node_attributes']['href'], 'ver=')) {
|
||||
$error['node_attributes']['href'] = \add_query_arg('ver', '__normalized__', $error['node_attributes']['href']);
|
||||
}
|
||||
} elseif ($node instanceof \DOMAttr) {
|
||||
if (!isset($error['code'])) {
|
||||
$error['code'] = \Google\Web_Stories_Dependencies\AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR;
|
||||
}
|
||||
if (!isset($error['type'])) {
|
||||
// If this is an attribute that begins with on, like onclick, it should be a js_error.
|
||||
$error['type'] = \preg_match('/^on\\w+/', $node->nodeName) ? \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::JS_ERROR_TYPE : \Google\Web_Stories_Dependencies\AMP_Validation_Error_Taxonomy::HTML_ATTRIBUTE_ERROR_TYPE;
|
||||
}
|
||||
if (!isset($error['element_attributes'])) {
|
||||
$error['element_attributes'] = [];
|
||||
if ($node->parentNode && $node->parentNode->hasAttributes()) {
|
||||
foreach ($node->parentNode->attributes as $attribute) {
|
||||
$error['element_attributes'][$attribute->nodeName] = $attribute->nodeValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($node instanceof \DOMProcessingInstruction) {
|
||||
$error['text'] = \trim($node->data, '?');
|
||||
}
|
||||
if (!isset($error['node_type'])) {
|
||||
$error['node_type'] = $node->nodeType;
|
||||
}
|
||||
return $error;
|
||||
}
|
||||
/**
|
||||
* Cleans up artifacts after the removal of an attribute node.
|
||||
*
|
||||
* @since 1.3
|
||||
*
|
||||
* @param DOMElement $element The node for which the attribute was removed.
|
||||
* @param DOMAttr $attribute The attribute that was removed.
|
||||
*/
|
||||
protected function clean_up_after_attribute_removal($element, $attribute)
|
||||
{
|
||||
static $attributes_tied_to_href = ['target', 'download', 'rel', 'rev', 'hreflang', 'type'];
|
||||
if ('href' === $attribute->nodeName) {
|
||||
/*
|
||||
* "The target, download, rel, rev, hreflang, and type attributes must be omitted
|
||||
* if the href attribute is not present."
|
||||
* See: https://www.w3.org/TR/2016/REC-html51-20161101/textlevel-semantics.html#the-a-element
|
||||
*/
|
||||
foreach ($attributes_tied_to_href as $attribute_to_remove) {
|
||||
if ($element->hasAttribute($attribute_to_remove)) {
|
||||
$element->removeAttribute($attribute_to_remove);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get data-amp-* values from the parent node 'figure' added by editor block.
|
||||
*
|
||||
* @param DOMElement $node Base node.
|
||||
* @return array AMP data array.
|
||||
*/
|
||||
public function get_data_amp_attributes($node)
|
||||
{
|
||||
$attributes = [];
|
||||
// Editor blocks add 'figure' as the parent node for images. If this node has data-amp-layout then we should add this as the layout attribute.
|
||||
$parent_node = $node->parentNode;
|
||||
if ($parent_node instanceof \DOMELement && 'figure' === $parent_node->tagName) {
|
||||
$parent_attributes = \Google\Web_Stories_Dependencies\AMP_DOM_Utils::get_node_attributes_as_assoc_array($parent_node);
|
||||
if (isset($parent_attributes['data-amp-layout'])) {
|
||||
$attributes['layout'] = $parent_attributes['data-amp-layout'];
|
||||
}
|
||||
if (isset($parent_attributes['data-amp-noloading']) && \true === \filter_var($parent_attributes['data-amp-noloading'], \FILTER_VALIDATE_BOOLEAN)) {
|
||||
$attributes['noloading'] = $parent_attributes['data-amp-noloading'];
|
||||
}
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
/**
|
||||
* Set AMP attributes.
|
||||
*
|
||||
* @param array $attributes Array of attributes.
|
||||
* @param array $amp_data Array of AMP attributes.
|
||||
* @return array Updated attributes.
|
||||
*/
|
||||
public function filter_data_amp_attributes($attributes, $amp_data)
|
||||
{
|
||||
if (isset($amp_data['layout'])) {
|
||||
$attributes['data-amp-layout'] = $amp_data['layout'];
|
||||
}
|
||||
if (isset($amp_data['noloading'])) {
|
||||
$attributes['data-amp-noloading'] = '';
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
/**
|
||||
* Set attributes to node's parent element according to layout.
|
||||
*
|
||||
* @param DOMElement $node Node.
|
||||
* @param array $new_attributes Attributes array.
|
||||
* @param string $layout Layout.
|
||||
* @return array New attributes.
|
||||
*/
|
||||
public function filter_attachment_layout_attributes($node, $new_attributes, $layout)
|
||||
{
|
||||
// The width has to be unset / auto in case of fixed-height.
|
||||
if ('fixed-height' === $layout && $node->parentNode instanceof \DOMElement) {
|
||||
if (!isset($new_attributes['height'])) {
|
||||
$new_attributes['height'] = self::FALLBACK_HEIGHT;
|
||||
}
|
||||
$new_attributes['width'] = 'auto';
|
||||
$node->parentNode->setAttribute('style', 'height: ' . $new_attributes['height'] . 'px; width: auto;');
|
||||
// The parent element should have width/height set and position set in case of 'fill'.
|
||||
} elseif ('fill' === $layout && $node->parentNode instanceof \DOMElement) {
|
||||
if (!isset($new_attributes['height'])) {
|
||||
$new_attributes['height'] = self::FALLBACK_HEIGHT;
|
||||
}
|
||||
$node->parentNode->setAttribute('style', 'position:relative; width: 100%; height: ' . $new_attributes['height'] . 'px;');
|
||||
unset($new_attributes['width'], $new_attributes['height']);
|
||||
} elseif ('responsive' === $layout && $node->parentNode instanceof \DOMElement) {
|
||||
$node->parentNode->setAttribute('style', 'position:relative; width: 100%; height: auto');
|
||||
} elseif ('fixed' === $layout) {
|
||||
if (!isset($new_attributes['height'])) {
|
||||
$new_attributes['height'] = self::FALLBACK_HEIGHT;
|
||||
}
|
||||
}
|
||||
return $new_attributes;
|
||||
}
|
||||
/**
|
||||
* Parse a style string into an associative array of style attributes.
|
||||
*
|
||||
* @param string $style_string Style string to parse.
|
||||
* @return string[] Associative array of style attributes.
|
||||
*/
|
||||
protected function parse_style_string($style_string)
|
||||
{
|
||||
// We need to turn the style string into an associative array of styles first.
|
||||
$style_string = \trim($style_string, " \t\n\r\x00\v;");
|
||||
$elements = \preg_split('/(\\s*:\\s*|\\s*;\\s*)/', $style_string);
|
||||
if (0 !== \count($elements) % 2) {
|
||||
// Style string was malformed, try to process as good as possible by stripping the last element.
|
||||
\array_pop($elements);
|
||||
}
|
||||
$chunks = \array_chunk($elements, 2);
|
||||
// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.array_columnFound -- WP Core provides a polyfill.
|
||||
return \array_combine(\array_column($chunks, 0), \array_column($chunks, 1));
|
||||
}
|
||||
/**
|
||||
* Reassemble a style string that can be used in a 'style' attribute.
|
||||
*
|
||||
* @param array $styles Associative array of styles to reassemble into a string.
|
||||
* @return string Reassembled style string.
|
||||
*/
|
||||
protected function reassemble_style_string($styles)
|
||||
{
|
||||
if (!\is_array($styles)) {
|
||||
return '';
|
||||
}
|
||||
// Discard empty values first.
|
||||
$styles = \array_filter($styles);
|
||||
return \array_reduce(\array_keys($styles), static function ($style_string, $style_name) use($styles) {
|
||||
if (!empty($style_string)) {
|
||||
$style_string .= ';';
|
||||
}
|
||||
return $style_string . "{$style_name}:{$styles[$style_name]}";
|
||||
}, '');
|
||||
}
|
||||
/**
|
||||
* Get data that is returned in validate responses.
|
||||
*
|
||||
* The array returned is merged with the overall validate response data.
|
||||
*
|
||||
* @see \AMP_Validation_Manager::get_validate_response_data()
|
||||
* @return array Validate response data.
|
||||
*/
|
||||
public function get_validate_response_data()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Google\Web_Stories_Dependencies;
|
||||
|
||||
/**
|
||||
* Class AMP_Dev_Mode_Sanitizer
|
||||
*
|
||||
* Add the data-ampdevmode to the document element and to the elements specified by the supplied args.
|
||||
*
|
||||
* @since 1.3
|
||||
* @package AMP
|
||||
*/
|
||||
/**
|
||||
* Class AMP_Dev_Mode_Sanitizer
|
||||
*
|
||||
* @since 1.3
|
||||
* @internal
|
||||
*/
|
||||
final class AMP_Dev_Mode_Sanitizer extends \Google\Web_Stories_Dependencies\AMP_Base_Sanitizer
|
||||
{
|
||||
/**
|
||||
* Array of flags used to control sanitization.
|
||||
*
|
||||
* @var array {
|
||||
* @type string[] $element_xpaths XPath expressions for elements to add the data-ampdevmode attribute to.
|
||||
* }
|
||||
*/
|
||||
protected $args;
|
||||
/**
|
||||
* Sanitize document for dev mode.
|
||||
*
|
||||
* @since 1.3
|
||||
*/
|
||||
public function sanitize()
|
||||
{
|
||||
$this->dom->documentElement->setAttribute(\Google\Web_Stories_Dependencies\AMP_Rule_Spec::DEV_MODE_ATTRIBUTE, '');
|
||||
$element_xpaths = !empty($this->args['element_xpaths']) ? $this->args['element_xpaths'] : [];
|
||||
foreach ($element_xpaths as $element_xpath) {
|
||||
foreach ($this->dom->xpath->query($element_xpath) as $node) {
|
||||
if ($node instanceof \DOMElement) {
|
||||
$node->setAttribute(\Google\Web_Stories_Dependencies\AMP_Rule_Spec::DEV_MODE_ATTRIBUTE, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Google\Web_Stories_Dependencies;
|
||||
|
||||
/**
|
||||
* Class AMP_Layout_Sanitizer
|
||||
*
|
||||
* @since 1.5.0
|
||||
* @package AMP
|
||||
*/
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Layout;
|
||||
/**
|
||||
* Class AMP_Layout_Sanitizer
|
||||
*
|
||||
* @since 1.5.0
|
||||
* @internal
|
||||
*/
|
||||
class AMP_Layout_Sanitizer extends \Google\Web_Stories_Dependencies\AMP_Base_Sanitizer
|
||||
{
|
||||
use \Google\Web_Stories_Dependencies\AMP_Noscript_Fallback;
|
||||
/**
|
||||
* Sanitize any element that has the `layout` or `data-amp-layout` attribute.
|
||||
*/
|
||||
public function sanitize()
|
||||
{
|
||||
$xpath = new \DOMXPath($this->dom);
|
||||
/**
|
||||
* Sanitize AMP nodes to be AMP compatible. Elements with the `layout` attribute will be validated by
|
||||
* `AMP_Tag_And_Attribute_Sanitizer`.
|
||||
*/
|
||||
$nodes = $xpath->query('//*[ starts-with( name(), "amp-" ) and not( @layout ) and ( @data-amp-layout or @width or @height or @style ) ]');
|
||||
foreach ($nodes as $node) {
|
||||
/**
|
||||
* Element.
|
||||
*
|
||||
* @var DOMElement $node
|
||||
*/
|
||||
// Layout does not apply inside of noscript.
|
||||
if ($this->is_inside_amp_noscript($node)) {
|
||||
continue;
|
||||
}
|
||||
$width = $node->getAttribute('width');
|
||||
$height = $node->getAttribute('height');
|
||||
$style = $node->getAttribute('style');
|
||||
// The `layout` attribute can also be defined through the `data-amp-layout` attribute.
|
||||
if ($node->hasAttribute('data-amp-layout')) {
|
||||
$layout = $node->getAttribute('data-amp-layout');
|
||||
$node->setAttribute('layout', $layout);
|
||||
$node->removeAttribute('data-amp-layout');
|
||||
}
|
||||
if (!$this->is_empty_attribute_value($style)) {
|
||||
$styles = $this->parse_style_string($style);
|
||||
/*
|
||||
* If both height & width descriptors are 100%, or
|
||||
* width attribute is 100% and height style descriptor is 100%, or
|
||||
* height attribute is 100% and width style descriptor is 100%
|
||||
* then apply fill layout.
|
||||
*/
|
||||
if (isset($styles['width'], $styles['height']) && ('100%' === $styles['width'] && '100%' === $styles['height']) || !$this->is_empty_attribute_value($width) && '100%' === $width && (isset($styles['height']) && '100%' === $styles['height']) || !$this->is_empty_attribute_value($height) && '100%' === $height && (isset($styles['width']) && '100%' === $styles['width'])) {
|
||||
unset($styles['width'], $styles['height']);
|
||||
$node->removeAttribute('width');
|
||||
$node->removeAttribute('height');
|
||||
if (empty($styles)) {
|
||||
$node->removeAttribute('style');
|
||||
} else {
|
||||
$node->setAttribute('style', $this->reassemble_style_string($styles));
|
||||
}
|
||||
$this->set_layout_fill($node);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// If the width & height are `100%` then apply fill layout.
|
||||
if ('100%' === $width && '100%' === $height) {
|
||||
$this->set_layout_fill($node);
|
||||
continue;
|
||||
}
|
||||
// If the width is `100%`, convert the layout to `fixed-height` and width to `auto`.
|
||||
if ('100%' === $width) {
|
||||
$node->setAttribute('width', 'auto');
|
||||
$node->setAttribute('layout', Layout::FIXED_HEIGHT);
|
||||
}
|
||||
}
|
||||
$this->did_convert_elements = \true;
|
||||
}
|
||||
/**
|
||||
* Apply the `fill` layout.
|
||||
*
|
||||
* @param DOMElement $node Node.
|
||||
*/
|
||||
private function set_layout_fill(\DOMElement $node)
|
||||
{
|
||||
if ($node->hasAttribute('width') && $node->hasAttribute('height')) {
|
||||
$node->removeAttribute('width');
|
||||
$node->removeAttribute('height');
|
||||
}
|
||||
if (Layout::FILL !== $node->getAttribute('layout')) {
|
||||
$node->setAttribute('layout', Layout::FILL);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
namespace Google\Web_Stories_Dependencies;
|
||||
|
||||
/**
|
||||
* Class AMP_Meta_Sanitizer.
|
||||
*
|
||||
* Ensure required markup is present for valid AMP pages.
|
||||
*
|
||||
* @todo Rename to something like AMP_Ensure_Required_Markup_Sanitizer.
|
||||
*
|
||||
* @link https://amp.dev/documentation/guides-and-tutorials/start/create/basic_markup/?format=websites#required-mark-up
|
||||
* @package AMP
|
||||
*/
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Encoding;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
|
||||
/**
|
||||
* Class AMP_Meta_Sanitizer.
|
||||
*
|
||||
* Sanitizes meta tags found in the header.
|
||||
*
|
||||
* @since 1.5.0
|
||||
* @internal
|
||||
*/
|
||||
class AMP_Meta_Sanitizer extends \Google\Web_Stories_Dependencies\AMP_Base_Sanitizer
|
||||
{
|
||||
/**
|
||||
* Default args.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $DEFAULT_ARGS = [
|
||||
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase
|
||||
'remove_initial_scale_viewport_property' => \true,
|
||||
];
|
||||
/**
|
||||
* Tag.
|
||||
*
|
||||
* @var string HTML <meta> tag to identify and replace with AMP version.
|
||||
*/
|
||||
public static $tag = 'meta';
|
||||
/*
|
||||
* Tags array keys.
|
||||
*/
|
||||
const TAG_CHARSET = 'charset';
|
||||
const TAG_HTTP_EQUIV = 'http-equiv';
|
||||
const TAG_VIEWPORT = 'viewport';
|
||||
const TAG_AMP_SCRIPT_SRC = 'amp_script_src';
|
||||
const TAG_OTHER = 'other';
|
||||
/**
|
||||
* Associative array of DOMElement arrays.
|
||||
*
|
||||
* Each key in the root level defines one group of meta tags to process.
|
||||
*
|
||||
* @var array $tags {
|
||||
* An array of meta tag groupings.
|
||||
*
|
||||
* @type DOMElement[] $charset Charset meta tag(s).
|
||||
* @type DOMElement[] $viewport Viewport meta tag(s).
|
||||
* @type DOMElement[] $amp_script_src <amp-script> source meta tags.
|
||||
* @type DOMElement[] $other Remaining meta tags.
|
||||
* }
|
||||
*/
|
||||
protected $meta_tags = [self::TAG_CHARSET => [], self::TAG_HTTP_EQUIV => [], self::TAG_VIEWPORT => [], self::TAG_AMP_SCRIPT_SRC => [], self::TAG_OTHER => []];
|
||||
/**
|
||||
* Viewport settings to use for AMP markup.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AMP_VIEWPORT = 'width=device-width';
|
||||
/**
|
||||
* Spec name for the tag spec for meta elements that are allowed in the body.
|
||||
*
|
||||
* @since 1.5.2
|
||||
* @var string
|
||||
*/
|
||||
const BODY_ANCESTOR_META_TAG_SPEC_NAME = 'meta name= and content=';
|
||||
/**
|
||||
* Get tag spec for meta tags which are allowed in the body.
|
||||
*
|
||||
* @since 1.5.2
|
||||
* @return string Deny pattern.
|
||||
*/
|
||||
private function get_body_meta_tag_name_attribute_deny_pattern()
|
||||
{
|
||||
static $pattern = null;
|
||||
if (null === $pattern) {
|
||||
$tag_spec = \current(\array_filter(\Google\Web_Stories_Dependencies\AMP_Allowed_Tags_Generated::get_allowed_tag('meta'), static function ($spec) {
|
||||
return isset($spec['tag_spec']['spec_name']) && self::BODY_ANCESTOR_META_TAG_SPEC_NAME === $spec['tag_spec']['spec_name'];
|
||||
}));
|
||||
$pattern = '/' . $tag_spec['attr_spec_list']['name']['disallowed_value_regex'] . '/';
|
||||
}
|
||||
return $pattern;
|
||||
}
|
||||
/**
|
||||
* Sanitize.
|
||||
*/
|
||||
public function sanitize()
|
||||
{
|
||||
$meta_elements = \iterator_to_array($this->dom->getElementsByTagName(static::$tag), \false);
|
||||
foreach ($meta_elements as $meta_element) {
|
||||
/**
|
||||
* Meta tag to process.
|
||||
*
|
||||
* @var DOMElement $meta_element
|
||||
*/
|
||||
if ($meta_element->hasAttribute(Attribute::CHARSET)) {
|
||||
$this->meta_tags[self::TAG_CHARSET][] = $meta_element->parentNode->removeChild($meta_element);
|
||||
} elseif ($meta_element->hasAttribute(Attribute::HTTP_EQUIV)) {
|
||||
$this->meta_tags[self::TAG_HTTP_EQUIV][] = $meta_element->parentNode->removeChild($meta_element);
|
||||
} elseif (Attribute::VIEWPORT === $meta_element->getAttribute(Attribute::NAME)) {
|
||||
$this->meta_tags[self::TAG_VIEWPORT][] = $meta_element->parentNode->removeChild($meta_element);
|
||||
} elseif (Attribute::AMP_SCRIPT_SRC === $meta_element->getAttribute(Attribute::NAME)) {
|
||||
$this->meta_tags[self::TAG_AMP_SCRIPT_SRC][] = $meta_element->parentNode->removeChild($meta_element);
|
||||
} elseif ($meta_element->hasAttribute('name') && \preg_match($this->get_body_meta_tag_name_attribute_deny_pattern(), $meta_element->getAttribute('name'))) {
|
||||
$this->meta_tags[self::TAG_OTHER][] = $meta_element->parentNode->removeChild($meta_element);
|
||||
}
|
||||
}
|
||||
$this->ensure_charset_is_present();
|
||||
$this->ensure_viewport_is_present();
|
||||
$this->process_amp_script_meta_tags();
|
||||
$this->re_add_meta_tags_in_optimized_order();
|
||||
$this->ensure_boilerplate_is_present();
|
||||
}
|
||||
/**
|
||||
* Always ensure that we have an HTML 5 charset meta tag.
|
||||
*
|
||||
* The charset is set to utf-8, which is what AMP requires.
|
||||
*/
|
||||
protected function ensure_charset_is_present()
|
||||
{
|
||||
if (!empty($this->meta_tags[self::TAG_CHARSET])) {
|
||||
return;
|
||||
}
|
||||
$this->meta_tags[self::TAG_CHARSET][] = $this->create_charset_element();
|
||||
}
|
||||
/**
|
||||
* Always ensure we have a viewport tag.
|
||||
*
|
||||
* The viewport defaults to 'width=device-width', which is the bare minimum that AMP requires.
|
||||
* If there are `@viewport` style rules, these will have been moved into the content attribute of their own meta[name=viewport]
|
||||
* tags by the style sanitizer. When there are multiple such meta tags, this method extracts the viewport properties of each
|
||||
* and then merges them into a single meta[name=viewport] tag. Any invalid properties will get removed by the
|
||||
* tag-and-attribute sanitizer.
|
||||
*/
|
||||
protected function ensure_viewport_is_present()
|
||||
{
|
||||
if (empty($this->meta_tags[self::TAG_VIEWPORT])) {
|
||||
$this->meta_tags[self::TAG_VIEWPORT][] = $this->create_viewport_element(static::AMP_VIEWPORT);
|
||||
} else {
|
||||
// Merge one or more meta[name=viewport] tags into one.
|
||||
$parsed_rules = [];
|
||||
/**
|
||||
* Meta viewport element.
|
||||
*
|
||||
* @var DOMElement $meta_viewport
|
||||
*/
|
||||
foreach ($this->meta_tags[self::TAG_VIEWPORT] as $meta_viewport) {
|
||||
$property_pairs = \explode(',', $meta_viewport->getAttribute('content'));
|
||||
foreach ($property_pairs as $property_pair) {
|
||||
$exploded_pair = \explode('=', $property_pair, 2);
|
||||
if (isset($exploded_pair[1])) {
|
||||
$parsed_rules[\trim($exploded_pair[0])] = \trim($exploded_pair[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove initial-scale=1 to leave just width=device-width in order to avoid a tap delay hurts FID.
|
||||
if (!empty($this->args['remove_initial_scale_viewport_property']) && isset($parsed_rules['initial-scale']) && \abs((float) $parsed_rules['initial-scale'] - 1.0) < 0.0001) {
|
||||
unset($parsed_rules['initial-scale']);
|
||||
}
|
||||
$viewport_value = \implode(',', \array_map(static function ($rule_name) use($parsed_rules) {
|
||||
return $rule_name . '=' . $parsed_rules[$rule_name];
|
||||
}, \array_keys($parsed_rules)));
|
||||
$this->meta_tags[self::TAG_VIEWPORT] = [$this->create_viewport_element($viewport_value)];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Always ensure we have a style[amp-boilerplate] and a noscript>style[amp-boilerplate].
|
||||
*
|
||||
* The AMP boilerplate styles should appear at the end of the head:
|
||||
* "Finally, specify the AMP boilerplate code. By putting the boilerplate code last, it prevents custom styles from
|
||||
* accidentally overriding the boilerplate css rules."
|
||||
*
|
||||
* @link https://amp.dev/documentation/guides-and-tutorials/learn/spec/amp-boilerplate/?format=websites
|
||||
* @link https://amp.dev/documentation/guides-and-tutorials/optimize-and-measure/optimize_amp/#optimize-the-amp-runtime-loading
|
||||
*/
|
||||
protected function ensure_boilerplate_is_present()
|
||||
{
|
||||
$style = $this->dom->xpath->query('./style[ @amp-boilerplate ]', $this->dom->head)->item(0);
|
||||
if (!$style) {
|
||||
$style = $this->dom->createElement(Tag::STYLE);
|
||||
$style->setAttribute(Attribute::AMP_BOILERPLATE, '');
|
||||
$style->appendChild($this->dom->createTextNode(\amp_get_boilerplate_stylesheets()[0]));
|
||||
} else {
|
||||
$style->parentNode->removeChild($style);
|
||||
// So we can move it.
|
||||
}
|
||||
$this->dom->head->appendChild($style);
|
||||
$noscript = $this->dom->xpath->query('./noscript[ style[ @amp-boilerplate ] ]', $this->dom->head)->item(0);
|
||||
if (!$noscript) {
|
||||
$noscript = $this->dom->createElement(Tag::NOSCRIPT);
|
||||
$style = $this->dom->createElement(Tag::STYLE);
|
||||
$style->setAttribute(Attribute::AMP_BOILERPLATE, '');
|
||||
$style->appendChild($this->dom->createTextNode(\amp_get_boilerplate_stylesheets()[1]));
|
||||
$noscript->appendChild($style);
|
||||
} else {
|
||||
$noscript->parentNode->removeChild($noscript);
|
||||
// So we can move it.
|
||||
}
|
||||
$this->dom->head->appendChild($noscript);
|
||||
}
|
||||
/**
|
||||
* Parse and concatenate <amp-script> source meta tags.
|
||||
*/
|
||||
protected function process_amp_script_meta_tags()
|
||||
{
|
||||
if (empty($this->meta_tags[self::TAG_AMP_SCRIPT_SRC])) {
|
||||
return;
|
||||
}
|
||||
$first_meta_amp_script_src = \array_shift($this->meta_tags[self::TAG_AMP_SCRIPT_SRC]);
|
||||
$content_values = [$first_meta_amp_script_src->getAttribute(Attribute::CONTENT)];
|
||||
// Merge (and remove) any subsequent meta amp-script-src elements.
|
||||
while (!empty($this->meta_tags[self::TAG_AMP_SCRIPT_SRC])) {
|
||||
$meta_amp_script_src = \array_shift($this->meta_tags[self::TAG_AMP_SCRIPT_SRC]);
|
||||
$content_values[] = $meta_amp_script_src->getAttribute(Attribute::CONTENT);
|
||||
}
|
||||
$first_meta_amp_script_src->setAttribute(Attribute::CONTENT, \implode(' ', $content_values));
|
||||
$this->meta_tags[self::TAG_AMP_SCRIPT_SRC][] = $first_meta_amp_script_src;
|
||||
}
|
||||
/**
|
||||
* Create a new meta tag for the charset value.
|
||||
*
|
||||
* @return DOMElement New meta tag with requested charset.
|
||||
*/
|
||||
protected function create_charset_element()
|
||||
{
|
||||
return \Google\Web_Stories_Dependencies\AMP_DOM_Utils::create_node($this->dom, Tag::META, [Attribute::CHARSET => Encoding::AMP]);
|
||||
}
|
||||
/**
|
||||
* Create a new meta tag for the viewport setting.
|
||||
*
|
||||
* @param string $viewport Viewport setting to use.
|
||||
* @return DOMElement New meta tag with requested viewport setting.
|
||||
*/
|
||||
protected function create_viewport_element($viewport)
|
||||
{
|
||||
return \Google\Web_Stories_Dependencies\AMP_DOM_Utils::create_node($this->dom, Tag::META, [Attribute::NAME => Attribute::VIEWPORT, Attribute::CONTENT => $viewport]);
|
||||
}
|
||||
/**
|
||||
* Re-add the meta tags to the <head> node in the optimized order.
|
||||
*
|
||||
* The order is defined by the array entries in $this->meta_tags.
|
||||
*
|
||||
* The optimal loading order for AMP pages is documented at:
|
||||
* https://amp.dev/documentation/guides-and-tutorials/optimize-and-measure/optimize_amp/#optimize-the-amp-runtime-loading
|
||||
*
|
||||
* "1. The first tag should be the meta charset tag, followed by any remaining meta tags."
|
||||
*/
|
||||
protected function re_add_meta_tags_in_optimized_order()
|
||||
{
|
||||
/**
|
||||
* Previous meta tag to append to.
|
||||
*
|
||||
* @var DOMElement|null $previous_meta_tag
|
||||
*/
|
||||
$previous_meta_tag = null;
|
||||
foreach ($this->meta_tags as $meta_tag_group) {
|
||||
foreach ($meta_tag_group as $meta_tag) {
|
||||
if ($previous_meta_tag) {
|
||||
$previous_meta_tag = $this->dom->head->insertBefore($meta_tag, $previous_meta_tag->nextSibling);
|
||||
} else {
|
||||
$previous_meta_tag = $this->dom->head->insertBefore($meta_tag, $this->dom->head->firstChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Google\Web_Stories_Dependencies;
|
||||
|
||||
/**
|
||||
* Class AMP_Rule_Spec
|
||||
*
|
||||
* @package AMP
|
||||
*/
|
||||
/**
|
||||
* Class AMP_Rule_Spec
|
||||
*
|
||||
* Set of constants used throughout the sanitizer.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class AMP_Rule_Spec
|
||||
{
|
||||
/*
|
||||
* AMP rule_spec types
|
||||
*/
|
||||
const ATTR_SPEC_LIST = 'attr_spec_list';
|
||||
const TAG_SPEC = 'tag_spec';
|
||||
const CDATA = 'cdata';
|
||||
/*
|
||||
* AMP attr_spec value check results.
|
||||
*
|
||||
* In 0.7 these changed from strings to integers to speed up comparisons.
|
||||
*/
|
||||
const PASS = 1;
|
||||
const FAIL = 0;
|
||||
const NOT_APPLICABLE = -1;
|
||||
/*
|
||||
* HTML Element Tag rule names
|
||||
*/
|
||||
const DISALLOWED_ANCESTOR = 'disallowed_ancestor';
|
||||
const MANDATORY_ANCESTOR = 'mandatory_ancestor';
|
||||
const MANDATORY_PARENT = 'mandatory_parent';
|
||||
const DESCENDANT_TAG_LIST = 'descendant_tag_list';
|
||||
const CHILD_TAGS = 'child_tags';
|
||||
const SIBLINGS_DISALLOWED = 'siblings_disallowed';
|
||||
/*
|
||||
* HTML Element Attribute rule names
|
||||
*/
|
||||
const ALLOW_EMPTY = 'allow_empty';
|
||||
const ALLOW_RELATIVE = 'allow_relative';
|
||||
const ALLOWED_PROTOCOL = 'protocol';
|
||||
const ALTERNATIVE_NAMES = 'alternative_names';
|
||||
const DISALLOWED_VALUE_REGEX = 'disallowed_value_regex';
|
||||
const MANDATORY = 'mandatory';
|
||||
const MANDATORY_ANYOF = 'mandatory_anyof';
|
||||
const MANDATORY_ONEOF = 'mandatory_oneof';
|
||||
const VALUE = 'value';
|
||||
const VALUE_CASEI = 'value_casei';
|
||||
const VALUE_REGEX = 'value_regex';
|
||||
const VALUE_REGEX_CASEI = 'value_regex_casei';
|
||||
const VALUE_PROPERTIES = 'value_properties';
|
||||
const VALUE_URL = 'value_url';
|
||||
/*
|
||||
* AMP layout types.
|
||||
*
|
||||
* @deprecated Use `AmpProject\Layout` interface instead.
|
||||
*/
|
||||
const LAYOUT_NODISPLAY = 'nodisplay';
|
||||
const LAYOUT_FIXED = 'fixed';
|
||||
const LAYOUT_FIXED_HEIGHT = 'fixed-height';
|
||||
const LAYOUT_RESPONSIVE = 'responsive';
|
||||
const LAYOUT_CONTAINER = 'container';
|
||||
const LAYOUT_FILL = 'fill';
|
||||
const LAYOUT_FLEX_ITEM = 'flex-item';
|
||||
const LAYOUT_FLUID = 'fluid';
|
||||
const LAYOUT_INTRINSIC = 'intrinsic';
|
||||
/**
|
||||
* Attribute name for AMP dev mode.
|
||||
*
|
||||
* @since 1.2.2
|
||||
* @link https://github.com/ampproject/amphtml/issues/20974
|
||||
* @var string
|
||||
*/
|
||||
const DEV_MODE_ATTRIBUTE = 'data-ampdevmode';
|
||||
/**
|
||||
* Supported layout values.
|
||||
*
|
||||
* @deprecated Use `AmpProject\Layout::FROM_SPEC` instead.
|
||||
*
|
||||
* @since 1.0
|
||||
* @var array
|
||||
*/
|
||||
public static $layout_enum = [1 => 'nodisplay', 2 => 'fixed', 3 => 'fixed-height', 4 => 'responsive', 5 => 'container', 6 => 'fill', 7 => 'flex-item', 8 => 'fluid', 9 => 'intrinsic'];
|
||||
/**
|
||||
* List of boolean attributes.
|
||||
*
|
||||
* @since 0.7
|
||||
* @var array
|
||||
*/
|
||||
public static $boolean_attributes = ['allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', 'default', 'defaultchecked', 'defaultmuted', 'defaultselected', 'defer', 'disabled', 'draggable', 'enabled', 'formnovalidate', 'hidden', 'indeterminate', 'inert', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nohref', 'noresize', 'noshade', 'novalidate', 'nowrap', 'open', 'pauseonexit', 'readonly', 'required', 'reversed', 'scoped', 'seamless', 'selected', 'sortable', 'spellcheck', 'translate', 'truespeed', 'typemustmatch', 'visible'];
|
||||
/**
|
||||
* Additional allowed tags.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $additional_allowed_tags = [
|
||||
// An experimental tag with no protoascii.
|
||||
'amp-share-tracking' => ['attr_spec_list' => [], 'tag_spec' => []],
|
||||
];
|
||||
}
|
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
|
||||
namespace Google\Web_Stories_Dependencies;
|
||||
|
||||
/**
|
||||
* Class AMP_Script_Sanitizer
|
||||
*
|
||||
* @since 1.0
|
||||
* @package AMP
|
||||
*/
|
||||
use Google\Web_Stories_Dependencies\AmpProject\AmpWP\ValidationExemption;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\DevMode;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Dom\Element;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Extension;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
|
||||
/**
|
||||
* Class AMP_Script_Sanitizer
|
||||
*
|
||||
* @since 1.0
|
||||
* @internal
|
||||
*/
|
||||
class AMP_Script_Sanitizer extends \Google\Web_Stories_Dependencies\AMP_Base_Sanitizer
|
||||
{
|
||||
/**
|
||||
* Error code for custom inline JS script tag.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CUSTOM_INLINE_SCRIPT = 'CUSTOM_INLINE_SCRIPT';
|
||||
/**
|
||||
* Error code for custom external JS script tag.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CUSTOM_EXTERNAL_SCRIPT = 'CUSTOM_EXTERNAL_SCRIPT';
|
||||
/**
|
||||
* Error code for JS event handler attribute.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CUSTOM_EVENT_HANDLER_ATTR = 'CUSTOM_EVENT_HANDLER_ATTR';
|
||||
/**
|
||||
* Attribute which if present on a `noscript` element will prevent it from being unwrapped.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const NO_UNWRAP_ATTR = 'data-amp-no-unwrap';
|
||||
/**
|
||||
* Default args.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $DEFAULT_ARGS = ['unwrap_noscripts' => \true, 'sanitize_js_scripts' => \false, 'comment_reply_allowed' => 'never'];
|
||||
/**
|
||||
* Array of flags used to control sanitization.
|
||||
*
|
||||
* @var array {
|
||||
* @type bool $sanitize_js_scripts Whether to sanitize JS scripts (and not defer for final sanitizer).
|
||||
* @type bool $unwrap_noscripts Whether to unwrap noscript elements.
|
||||
* }
|
||||
*/
|
||||
protected $args;
|
||||
/**
|
||||
* Number of scripts which were kept.
|
||||
*
|
||||
* This is used to determine whether noscript elements should be unwrapped.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $kept_script_count = 0;
|
||||
/**
|
||||
* Number of kept nodes which were PX-verified.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $px_verified_kept_node_count = 0;
|
||||
/**
|
||||
* Sanitizers.
|
||||
*
|
||||
* @var AMP_Base_Sanitizer[]
|
||||
*/
|
||||
protected $sanitizers = [];
|
||||
/**
|
||||
* Init.
|
||||
*
|
||||
* @param AMP_Base_Sanitizer[] $sanitizers Sanitizers.
|
||||
*/
|
||||
public function init($sanitizers)
|
||||
{
|
||||
parent::init($sanitizers);
|
||||
$this->sanitizers = $sanitizers;
|
||||
}
|
||||
/**
|
||||
* Sanitize script and noscript elements.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public function sanitize()
|
||||
{
|
||||
if (!empty($this->args['sanitize_js_scripts'])) {
|
||||
$this->sanitize_js_script_elements();
|
||||
}
|
||||
// If custom scripts were kept (after sanitize_js_script_elements() ran) it's important that noscripts not be
|
||||
// unwrapped or else this could result in the JS and no-JS fallback experiences both being present on the page.
|
||||
// So unwrapping is only done when no custom scripts were retained (and the sanitizer arg opts-in to unwrap).
|
||||
if (0 === $this->kept_script_count && 0 === $this->px_verified_kept_node_count && !empty($this->args['unwrap_noscripts'])) {
|
||||
$this->unwrap_noscript_elements();
|
||||
}
|
||||
$sanitizer_arg_updates = [];
|
||||
// When there are kept custom scripts, turn off conversion to AMP components since scripts may be attempting to
|
||||
// query for them directly, and skip tree shaking since it's likely JS will toggle classes that have associated
|
||||
// style rules.
|
||||
// @todo There should be an attribute on script tags that opt-in to keeping tree shaking and/or to indicate what class names need to be included.
|
||||
if ($this->kept_script_count > 0) {
|
||||
$sanitizer_arg_updates[\Google\Web_Stories_Dependencies\AMP_Style_Sanitizer::class]['disable_style_processing'] = \true;
|
||||
$sanitizer_arg_updates[\Google\Web_Stories_Dependencies\AMP_Video_Sanitizer::class]['native_video_used'] = \true;
|
||||
$sanitizer_arg_updates[\Google\Web_Stories_Dependencies\AMP_Audio_Sanitizer::class]['native_audio_used'] = \true;
|
||||
$sanitizer_arg_updates[\Google\Web_Stories_Dependencies\AMP_Iframe_Sanitizer::class]['native_iframe_used'] = \true;
|
||||
// Once amp-img is deprecated, these won't be needed and an <img> won't prevent strict sandboxing level for valid AMP.
|
||||
// Note that AMP_Core_Theme_Sanitizer would have already run, so we can't update it here. Nevertheless,
|
||||
// the native_img_used flag was already enabled by the Sandboxing service.
|
||||
// @todo We should consider doing this when there are PX-verified scripts as well. This will be the default in AMP eventually anyway, as amp-img is being deprecated.
|
||||
$sanitizer_arg_updates[\Google\Web_Stories_Dependencies\AMP_Gallery_Block_Sanitizer::class]['native_img_used'] = \true;
|
||||
$sanitizer_arg_updates[\Google\Web_Stories_Dependencies\AMP_Img_Sanitizer::class]['native_img_used'] = \true;
|
||||
}
|
||||
// When custom scripts are on the page, turn off some CSS processing because it is
|
||||
// unnecessary for a valid AMP page and which can break custom scripts.
|
||||
if ($this->px_verified_kept_node_count > 0 || $this->kept_script_count > 0) {
|
||||
$sanitizer_arg_updates[\Google\Web_Stories_Dependencies\AMP_Style_Sanitizer::class]['transform_important_qualifiers'] = \false;
|
||||
$sanitizer_arg_updates[\Google\Web_Stories_Dependencies\AMP_Style_Sanitizer::class]['allow_excessive_css'] = \true;
|
||||
$sanitizer_arg_updates[\Google\Web_Stories_Dependencies\AMP_Form_Sanitizer::class]['native_post_forms_allowed'] = 'always';
|
||||
}
|
||||
foreach ($sanitizer_arg_updates as $sanitizer_class => $sanitizer_args) {
|
||||
if (\array_key_exists($sanitizer_class, $this->sanitizers)) {
|
||||
$this->sanitizers[$sanitizer_class]->update_args($sanitizer_args);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Unwrap noscript elements so their contents become the AMP version by default.
|
||||
*/
|
||||
protected function unwrap_noscript_elements()
|
||||
{
|
||||
$noscripts = $this->dom->getElementsByTagName(Tag::NOSCRIPT);
|
||||
for ($i = $noscripts->length - 1; $i >= 0; $i--) {
|
||||
/** @var Element $noscript */
|
||||
$noscript = $noscripts->item($i);
|
||||
// Skip AMP boilerplate.
|
||||
if ($noscript->firstChild instanceof Element && $noscript->firstChild->hasAttribute(Attribute::AMP_BOILERPLATE)) {
|
||||
continue;
|
||||
}
|
||||
// Skip unwrapping <noscript> elements that have an opt-out data attribute present.
|
||||
if ($noscript->hasAttribute(self::NO_UNWRAP_ATTR)) {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* Skip noscript elements inside of amp-img or other AMP components for fallbacks.
|
||||
* See \AMP_Img_Sanitizer::adjust_and_replace_node(). Also skip if the element has dev mode.
|
||||
*/
|
||||
if ('amp-' === \substr($noscript->parentNode->nodeName, 0, 4) || DevMode::hasExemptionForNode($noscript)) {
|
||||
continue;
|
||||
}
|
||||
$is_inside_head_el = $noscript->parentNode && Tag::HEAD === $noscript->parentNode->nodeName;
|
||||
$must_move_to_body = \false;
|
||||
$fragment = $this->dom->createDocumentFragment();
|
||||
$fragment->appendChild($this->dom->createComment('noscript'));
|
||||
while ($noscript->firstChild) {
|
||||
if ($is_inside_head_el && !$must_move_to_body) {
|
||||
$must_move_to_body = !$this->dom->isValidHeadNode($noscript->firstChild);
|
||||
}
|
||||
$fragment->appendChild($noscript->firstChild);
|
||||
}
|
||||
$fragment->appendChild($this->dom->createComment('/noscript'));
|
||||
if ($must_move_to_body) {
|
||||
$this->dom->body->insertBefore($fragment, $this->dom->body->firstChild);
|
||||
$noscript->parentNode->removeChild($noscript);
|
||||
} else {
|
||||
$noscript->parentNode->replaceChild($fragment, $noscript);
|
||||
}
|
||||
$this->did_convert_elements = \true;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sanitize JavaScript script elements.
|
||||
*
|
||||
* This runs explicitly in the script sanitizer before the final validating sanitizer (tag-and-attribute) so that
|
||||
* the style sanitizer will be able to know whether there are custom scripts in the page, and thus whether tree
|
||||
* shaking can be performed.
|
||||
*
|
||||
* @since 2.2
|
||||
*/
|
||||
protected function sanitize_js_script_elements()
|
||||
{
|
||||
// Note that this is looking for type attributes that contain "script" as a normalization of the variations
|
||||
// of javascript, ecmascript, jscript, and livescript. This could also end up matching MIME types such as
|
||||
// application/postscript or text/vbscript, but such scripts are either unlikely to be the source of a script
|
||||
// tag or else they would be extremely rare (and would be invalid AMP anyway).
|
||||
// See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#textjavascript>.
|
||||
$scripts = $this->dom->xpath->query('
|
||||
//script[
|
||||
not( @type )
|
||||
or
|
||||
@type = "module"
|
||||
or
|
||||
contains( @type, "script" )
|
||||
]');
|
||||
$comment_reply_script = null;
|
||||
/** @var Element $script */
|
||||
foreach ($scripts as $script) {
|
||||
if (DevMode::hasExemptionForNode($script)) {
|
||||
// @todo Should this also skip when AMP-unvalidated?
|
||||
continue;
|
||||
}
|
||||
if (ValidationExemption::is_px_verified_for_node($script)) {
|
||||
$this->px_verified_kept_node_count++;
|
||||
// @todo Consider forcing any PX-verified script to have async/defer if not module. For inline scripts, hack via data: URL?
|
||||
continue;
|
||||
}
|
||||
if ($script->hasAttribute(Attribute::SRC)) {
|
||||
// Skip external AMP CDN scripts.
|
||||
if (0 === \strpos($script->getAttribute(Attribute::SRC), 'https://cdn.ampproject.org/')) {
|
||||
continue;
|
||||
}
|
||||
// Defer consideration of commenting scripts until we've seen what other scripts are kept on the page.
|
||||
if ($script->getAttribute(Attribute::ID) === 'comment-reply-js') {
|
||||
$comment_reply_script = $script;
|
||||
continue;
|
||||
}
|
||||
$removed = $this->remove_invalid_child($script, ['code' => self::CUSTOM_EXTERNAL_SCRIPT]);
|
||||
if (!$removed) {
|
||||
$this->kept_script_count++;
|
||||
}
|
||||
} else {
|
||||
// Skip inline scripts used by AMP.
|
||||
if ($script->hasAttribute(Attribute::AMP_ONERROR)) {
|
||||
continue;
|
||||
}
|
||||
// As a special case, mark the script output by wp_comment_form_unfiltered_html_nonce() as being in dev-mode
|
||||
// since it is output when the user is authenticated (when they can unfiltered_html), and since it has no
|
||||
// impact on PX we can just ignore it.
|
||||
if ($script->previousSibling instanceof Element && Tag::INPUT === $script->previousSibling->tagName && $script->previousSibling->getAttribute(Attribute::NAME) === '_wp_unfiltered_html_comment_disabled') {
|
||||
$script->setAttributeNode($this->dom->createAttribute(DevMode::DEV_MODE_ATTRIBUTE));
|
||||
continue;
|
||||
}
|
||||
$removed = $this->remove_invalid_child($script, ['code' => self::CUSTOM_INLINE_SCRIPT]);
|
||||
if (!$removed) {
|
||||
$this->kept_script_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
$event_handler_attributes = $this->dom->xpath->query('
|
||||
//@*[
|
||||
substring(name(), 1, 2) = "on"
|
||||
and
|
||||
name() != "on"
|
||||
]
|
||||
');
|
||||
/** @var DOMAttr $event_handler_attribute */
|
||||
foreach ($event_handler_attributes as $event_handler_attribute) {
|
||||
/** @var Element $element */
|
||||
$element = $event_handler_attribute->parentNode;
|
||||
if (DevMode::hasExemptionForNode($element) || Extension::POSITION_OBSERVER === $element->tagName && Attribute::ONCE === $event_handler_attribute->nodeName || Extension::FONT === $element->tagName && \substr($event_handler_attribute->nodeName, 0, 3) === 'on-') {
|
||||
continue;
|
||||
}
|
||||
// Since the attribute has been PX-verified, move along.
|
||||
if (ValidationExemption::is_px_verified_for_node($event_handler_attribute)) {
|
||||
$this->px_verified_kept_node_count++;
|
||||
continue;
|
||||
}
|
||||
$removed = $this->remove_invalid_attribute($element, $event_handler_attribute, ['code' => self::CUSTOM_EVENT_HANDLER_ATTR]);
|
||||
if (!$removed) {
|
||||
$this->kept_script_count++;
|
||||
}
|
||||
}
|
||||
// Handle the comment-reply script, removing it if it's not never allowed, marking it as PX-verified if it is
|
||||
// always allowed, or leaving it alone if it is 'conditionally' allowed since it will be dealt with later
|
||||
// in the AMP_Comments_Sanitizer.
|
||||
if ($comment_reply_script) {
|
||||
if ('never' === $this->args['comment_reply_allowed']) {
|
||||
$comment_reply_script->parentNode->removeChild($comment_reply_script);
|
||||
} elseif ('always' === $this->args['comment_reply_allowed']) {
|
||||
// Prevent the comment-reply script from being removed later in the comments sanitizer.
|
||||
ValidationExemption::mark_node_as_px_verified($comment_reply_script);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Google\Web_Stories_Dependencies;
|
||||
|
||||
/**
|
||||
* Trait AMP_Noscript_Fallback.
|
||||
*
|
||||
* @package AMP
|
||||
*/
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
|
||||
/**
|
||||
* Trait AMP_Noscript_Fallback
|
||||
*
|
||||
* Used for sanitizers that place <noscript> tags with the original nodes on error.
|
||||
*
|
||||
* @since 1.1
|
||||
* @internal
|
||||
*/
|
||||
trait AMP_Noscript_Fallback
|
||||
{
|
||||
/**
|
||||
* Attributes allowed on noscript fallback elements.
|
||||
*
|
||||
* This is used to prevent duplicated validation errors.
|
||||
*
|
||||
* @since 1.1
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $noscript_fallback_allowed_attributes = [];
|
||||
/**
|
||||
* Initializes the internal allowed attributes array.
|
||||
*
|
||||
* @since 1.1
|
||||
*
|
||||
* @param string $tag Tag name to get allowed attributes for.
|
||||
*/
|
||||
protected function initialize_noscript_allowed_attributes($tag)
|
||||
{
|
||||
$this->noscript_fallback_allowed_attributes = \array_fill_keys(\array_keys(\Google\Web_Stories_Dependencies\AMP_Allowed_Tags_Generated::get_allowed_attributes()), \true);
|
||||
foreach (\Google\Web_Stories_Dependencies\AMP_Allowed_Tags_Generated::get_allowed_tag($tag) as $tag_spec) {
|
||||
// Normally 1 iteration.
|
||||
if (!(isset($tag_spec['tag_spec']['mandatory_ancestor']) && Tag::NOSCRIPT === $tag_spec['tag_spec']['mandatory_ancestor'] || isset($tag_spec['tag_spec']['mandatory_parent']) && Tag::NOSCRIPT === $tag_spec['tag_spec']['mandatory_parent'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($tag_spec['attr_spec_list'] as $attr_name => $attr_spec) {
|
||||
$this->noscript_fallback_allowed_attributes[$attr_name] = \true;
|
||||
if (isset($attr_spec['alternative_names'])) {
|
||||
$this->noscript_fallback_allowed_attributes = \array_merge($this->noscript_fallback_allowed_attributes, \array_fill_keys($attr_spec['alternative_names'], \true));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove attributes which are likely to cause styling conflicts, as the noscript fallback should get treated like it has fill layout.
|
||||
unset($this->noscript_fallback_allowed_attributes[Attribute::ID], $this->noscript_fallback_allowed_attributes[Attribute::CLASS_], $this->noscript_fallback_allowed_attributes[Attribute::STYLE]);
|
||||
}
|
||||
/**
|
||||
* Checks whether the given node is within an AMP-specific <noscript> element.
|
||||
*
|
||||
* @since 1.1
|
||||
*
|
||||
* @param DOMNode $node DOM node to check.
|
||||
*
|
||||
* @return bool True if in an AMP noscript element, false otherwise.
|
||||
*/
|
||||
protected function is_inside_amp_noscript(\DOMNode $node)
|
||||
{
|
||||
return 'noscript' === $node->parentNode->nodeName && $node->parentNode->parentNode && 'amp-' === \substr($node->parentNode->parentNode->nodeName, 0, 4);
|
||||
}
|
||||
/**
|
||||
* Appends the given old node in a <noscript> element to the new node.
|
||||
*
|
||||
* @since 1.1
|
||||
*
|
||||
* @param DOMElement $new_element New element to append a noscript with the old element to.
|
||||
* @param DOMElement $old_element Old element to append in a noscript.
|
||||
* @param Document $dom DOM document instance.
|
||||
*/
|
||||
protected function append_old_node_noscript(\DOMElement $new_element, \DOMElement $old_element, Document $dom)
|
||||
{
|
||||
$noscript = $dom->createElement('noscript');
|
||||
$noscript->appendChild($old_element);
|
||||
$new_element->appendChild($noscript);
|
||||
// Remove all non-allowed attributes preemptively to prevent doubled validation errors, only leaving the attributes required.
|
||||
for ($i = $old_element->attributes->length - 1; $i >= 0; $i--) {
|
||||
$attribute = $old_element->attributes->item($i);
|
||||
if (!isset($this->noscript_fallback_allowed_attributes[$attribute->nodeName])) {
|
||||
$old_element->removeAttribute($attribute->nodeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Google\Web_Stories_Dependencies;
|
||||
|
||||
/**
|
||||
* Class AMP_Content_Sanitizer
|
||||
*
|
||||
* @package AMP
|
||||
*/
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
|
||||
/**
|
||||
* Class AMP_Content_Sanitizer
|
||||
*
|
||||
* @since 0.4.1
|
||||
* @internal
|
||||
*/
|
||||
class AMP_Content_Sanitizer
|
||||
{
|
||||
/**
|
||||
* Sanitize _content_.
|
||||
*
|
||||
* @since 0.4.1
|
||||
* @since 0.7 Passing return_styles=false in $global_args causes stylesheets to be returned instead of styles.
|
||||
* @codeCoverageIgnore
|
||||
* @deprecated Since 1.0
|
||||
*
|
||||
* @param string $content HTML content string or DOM document.
|
||||
* @param array[] $sanitizer_classes Sanitizers, with keys as class names and values as arguments.
|
||||
* @param array $global_args Global args.
|
||||
* @return array Tuple containing sanitized HTML, scripts array, and styles array (or stylesheets, if return_styles=false is passed in $global_args).
|
||||
*/
|
||||
public static function sanitize($content, array $sanitizer_classes, $global_args = [])
|
||||
{
|
||||
$dom = \Google\Web_Stories_Dependencies\AMP_DOM_Utils::get_dom_from_content($content);
|
||||
// For back-compat.
|
||||
if (!isset($global_args['return_styles'])) {
|
||||
$global_args['return_styles'] = \true;
|
||||
}
|
||||
$results = self::sanitize_document($dom, $sanitizer_classes, $global_args);
|
||||
return [\Google\Web_Stories_Dependencies\AMP_DOM_Utils::get_content_from_dom($dom), $results['scripts'], empty($global_args['return_styles']) ? $results['stylesheets'] : $results['styles']];
|
||||
}
|
||||
/**
|
||||
* Sanitize document.
|
||||
*
|
||||
* @since 0.7
|
||||
*
|
||||
* @param Document $dom HTML document.
|
||||
* @param array[] $sanitizer_classes Sanitizers, with keys as class names and values as arguments.
|
||||
* @param array $args Global args passed into sanitizers.
|
||||
* @return array {
|
||||
* Scripts and stylesheets needed by sanitizers.
|
||||
*
|
||||
* @type array $scripts Scripts.
|
||||
* @type array $stylesheets Stylesheets. If $args['return_styles'] is empty.
|
||||
* @type array $styles Styles. If $args['return_styles'] is not empty. For legacy purposes.
|
||||
* @type AMP_Base_Sanitizer[] $sanitizers Sanitizers.
|
||||
* }
|
||||
*/
|
||||
public static function sanitize_document(Document $dom, $sanitizer_classes, $args)
|
||||
{
|
||||
$scripts = [];
|
||||
$stylesheets = [];
|
||||
$styles = [];
|
||||
$return_styles = !empty($args['return_styles']);
|
||||
unset($args['return_styles']);
|
||||
/**
|
||||
* Sanitizers.
|
||||
*
|
||||
* @var AMP_Base_Sanitizer[] $sanitizers
|
||||
*/
|
||||
$sanitizers = [];
|
||||
// Instantiate the sanitizers.
|
||||
foreach ($sanitizer_classes as $sanitizer_class => $sanitizer_args) {
|
||||
if (!\class_exists($sanitizer_class)) {
|
||||
/* translators: %s is sanitizer class */
|
||||
\_doing_it_wrong(__METHOD__, \sprintf(\esc_html__('Sanitizer (%s) class does not exist', 'amp'), \esc_html($sanitizer_class)), '0.4.1');
|
||||
continue;
|
||||
}
|
||||
/**
|
||||
* Sanitizer.
|
||||
*
|
||||
* @type AMP_Base_Sanitizer $sanitizer
|
||||
*/
|
||||
$sanitizer = new $sanitizer_class($dom, \array_merge($args, $sanitizer_args));
|
||||
if (!$sanitizer instanceof \Google\Web_Stories_Dependencies\AMP_Base_Sanitizer) {
|
||||
\_doing_it_wrong(__METHOD__, \esc_html(\sprintf(
|
||||
/* translators: 1: sanitizer class. 2: AMP_Base_Sanitizer */
|
||||
\__('Sanitizer (%1$s) must extend `%2$s`', 'amp'),
|
||||
\esc_html($sanitizer_class),
|
||||
\Google\Web_Stories_Dependencies\AMP_Base_Sanitizer::class
|
||||
)), '0.1');
|
||||
continue;
|
||||
}
|
||||
$sanitizers[$sanitizer_class] = $sanitizer;
|
||||
}
|
||||
// Let the sanitizers know about each other prior to sanitizing.
|
||||
foreach ($sanitizers as $sanitizer) {
|
||||
$sanitizer->init($sanitizers);
|
||||
}
|
||||
// Sanitize.
|
||||
$sanitizers_to_surface = [\Google\Web_Stories_Dependencies\AMP_Style_Sanitizer::class, \Google\Web_Stories_Dependencies\AMP_Tag_And_Attribute_Sanitizer::class];
|
||||
foreach ($sanitizers as $sanitizer_class => $sanitizer) {
|
||||
/**
|
||||
* Starts the server-timing measurement for an individual sanitizer.
|
||||
*
|
||||
* @since 2.0
|
||||
* @internal
|
||||
*
|
||||
* @param string $event_name Name of the event to record.
|
||||
* @param string|null $event_description Optional. Description of the event
|
||||
* to record. Defaults to null.
|
||||
* @param string[] $properties Optional. Additional properties to add
|
||||
* to the logged record.
|
||||
* @param bool $verbose_only Optional. Whether to only show the
|
||||
* event in verbose mode. Defaults to
|
||||
* false.
|
||||
*/
|
||||
\do_action('amp_server_timing_start', \strtolower($sanitizer_class), '', [], !\in_array($sanitizer_class, $sanitizers_to_surface, \true));
|
||||
$sanitizer->sanitize();
|
||||
$scripts = \array_merge($scripts, $sanitizer->get_scripts());
|
||||
if ($return_styles) {
|
||||
$styles = \array_merge($styles, $sanitizer->get_styles());
|
||||
} else {
|
||||
$stylesheets = \array_merge($stylesheets, $sanitizer->get_stylesheets());
|
||||
}
|
||||
/**
|
||||
* Stops the server-timing measurement for an individual sanitizer.
|
||||
*
|
||||
* @since 2.0
|
||||
* @internal
|
||||
*
|
||||
* @param string $event_name Name of the event to stop.
|
||||
*/
|
||||
\do_action('amp_server_timing_stop', \strtolower($sanitizer_class));
|
||||
}
|
||||
return \compact('scripts', 'styles', 'stylesheets', 'sanitizers');
|
||||
}
|
||||
}
|
142
wp-content/plugins/web-stories/third-party/vendor/ampproject/amp-wp/includes/uninstall-functions.php
vendored
Normal file
142
wp-content/plugins/web-stories/third-party/vendor/ampproject/amp-wp/includes/uninstall-functions.php
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Helper function for removing plugin data during uninstalling.
|
||||
*
|
||||
* @package AMP
|
||||
*/
|
||||
namespace Google\Web_Stories_Dependencies\AmpProject\AmpWP;
|
||||
|
||||
/**
|
||||
* Delete data from option table.
|
||||
*
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
function delete_options()
|
||||
{
|
||||
$options = \get_option('amp-options');
|
||||
\delete_option('amp-options');
|
||||
\delete_option('amp_css_transient_monitor_time_series');
|
||||
\delete_option('amp_url_validation_queue');
|
||||
// See Validation\URLValidationCron::OPTION_KEY.
|
||||
$theme_mod_name = 'amp_customize_setting_modified_timestamps';
|
||||
\remove_theme_mod($theme_mod_name);
|
||||
if (!empty($options['reader_theme']) && 'legacy' !== $options['reader_theme']) {
|
||||
$reader_theme_mods_option_name = \sprintf('theme_mods_%s', $options['reader_theme']);
|
||||
$reader_theme_mods = \get_option($reader_theme_mods_option_name);
|
||||
if (\is_array($reader_theme_mods) && isset($reader_theme_mods[$theme_mod_name])) {
|
||||
unset($reader_theme_mods[$theme_mod_name]);
|
||||
\update_option($reader_theme_mods_option_name, $reader_theme_mods);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Delete AMP user meta.
|
||||
*
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
function delete_user_metadata()
|
||||
{
|
||||
$keys = ['amp_dev_tools_enabled', 'amp_review_panel_dismissed_for_template_mode'];
|
||||
foreach ($keys as $key) {
|
||||
\delete_metadata('user', 0, $key, '', \true);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Delete AMP Validated URL posts.
|
||||
*
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
function delete_posts()
|
||||
{
|
||||
/** @var \wpdb */
|
||||
global $wpdb;
|
||||
$post_type = 'amp_validated_url';
|
||||
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
// Delete all post meta data related to "amp_validated_url" post_type.
|
||||
$wpdb->query($wpdb->prepare("\n\t\t\tDELETE meta\n\t\t\tFROM {$wpdb->postmeta} AS meta\n\t\t\t\tINNER JOIN {$wpdb->posts} AS posts\n\t\t\t\t\tON posts.ID = meta.post_id\n\t\t\tWHERE posts.post_type = %s;\n\t\t\t", $post_type));
|
||||
// Delete all amp_validated_url posts.
|
||||
$wpdb->delete($wpdb->posts, \compact('post_type'));
|
||||
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
}
|
||||
/**
|
||||
* Delete AMP validation error terms.
|
||||
*
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
function delete_terms()
|
||||
{
|
||||
// Abort if term splitting has not been done. This is done by WooCommerce so it's
|
||||
// it's also done here for good measure, even though we require WP 4.9+.
|
||||
if (\version_compare(\get_bloginfo('version'), '4.2', '<')) {
|
||||
return;
|
||||
}
|
||||
/** @var \wpdb */
|
||||
global $wpdb;
|
||||
$taxonomy = 'amp_validation_error';
|
||||
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
// Delete term meta (added in WP 4.4).
|
||||
if (!empty($wpdb->termmeta)) {
|
||||
$wpdb->query($wpdb->prepare("\n\t\t\t\tDELETE tm\n\t\t\t\tFROM {$wpdb->termmeta} AS tm\n\t\t\t\t\tINNER JOIN {$wpdb->term_taxonomy} AS tt\n\t\t\t\t\t\tON tm.term_id = tt.term_id\n\t\t\t\tWHERE tt.taxonomy = %s;\n\t\t\t\t", $taxonomy));
|
||||
}
|
||||
// Delete term relationship.
|
||||
$wpdb->query($wpdb->prepare("\n\t\t\tDELETE tr\n\t\t\tFROM {$wpdb->term_relationships} AS tr\n\t\t\t\tINNER JOIN {$wpdb->term_taxonomy} AS tt\n\t\t\t\t\tON tr.term_taxonomy_id = tt.term_taxonomy_id\n\t\t\tWHERE tt.taxonomy = %s;\n\t\t\t", $taxonomy));
|
||||
// Delete terms.
|
||||
$wpdb->query($wpdb->prepare("\n\t\t\tDELETE terms\n\t\t\tFROM {$wpdb->terms} AS terms\n\t\t\t\tINNER JOIN {$wpdb->term_taxonomy} AS tt\n\t\t\t\t\tON terms.term_id = tt.term_id\n\t\t\tWHERE tt.taxonomy = %s;\n\t\t\t", $taxonomy));
|
||||
// Delete term taxonomy.
|
||||
$wpdb->delete($wpdb->term_taxonomy, \compact('taxonomy'));
|
||||
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
}
|
||||
/**
|
||||
* Delete transient data from option table if object cache is not available.
|
||||
*
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
function delete_transients()
|
||||
{
|
||||
// Transients are not stored in the options table if an external object cache is used,
|
||||
// in which case they cannot be queried for deletion.
|
||||
if (\wp_using_ext_object_cache()) {
|
||||
return;
|
||||
}
|
||||
/** @var \wpdb */
|
||||
global $wpdb;
|
||||
$transient_groups = ['Google\\Web_Stories_Dependencies\\AmpProject\\AmpWP\\DevTools\\BlockSourcesamp_block_sources', 'amp-parsed-stylesheet-v%', 'amp_error_index_counts', 'amp_has_page_caching', 'amp_img_%', 'amp_lock_%', 'amp_new_validation_error_urls_count', 'amp_plugin_activation_validation_errors', 'amp_remote_request_%', 'amp_themes_wporg'];
|
||||
$where_clause = [];
|
||||
foreach ($transient_groups as $transient_group) {
|
||||
if (\false !== \strpos($transient_group, '%')) {
|
||||
$where_clause[] = $wpdb->prepare(' option_name LIKE %s OR option_name LIKE %s ', "_transient_{$transient_group}", "_transient_timeout_{$transient_group}");
|
||||
} else {
|
||||
$where_clause[] = $wpdb->prepare(' option_name = %s OR option_name = %s ', "_transient_{$transient_group}", "_transient_timeout_{$transient_group}");
|
||||
}
|
||||
}
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cannot cache result since we're deleting the records.
|
||||
$wpdb->query(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- See use of prepare in foreach loop above.
|
||||
"DELETE FROM {$wpdb->options} WHERE " . \implode(' OR ', $where_clause)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Remove plugin data.
|
||||
*
|
||||
* @return void
|
||||
* @internal
|
||||
*/
|
||||
function remove_plugin_data()
|
||||
{
|
||||
$options = \get_option('amp-options');
|
||||
if (\is_array($options) && \array_key_exists('delete_data_at_uninstall', $options) ? $options['delete_data_at_uninstall'] : \true) {
|
||||
\delete_options();
|
||||
\delete_user_metadata();
|
||||
\delete_posts();
|
||||
\delete_terms();
|
||||
\delete_transients();
|
||||
// Clear any cached data that has been removed.
|
||||
\wp_cache_flush();
|
||||
}
|
||||
}
|
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
|
||||
namespace Google\Web_Stories_Dependencies;
|
||||
|
||||
/**
|
||||
* Class AMP_DOM_Utils.
|
||||
*
|
||||
* @package AMP
|
||||
*/
|
||||
use Google\Web_Stories_Dependencies\AmpProject\AmpWP\Dom\Options;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Dom\Element;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
|
||||
/**
|
||||
* Class AMP_DOM_Utils
|
||||
*
|
||||
* Functionality to simplify working with Dom\Documents and DOMElements.
|
||||
*
|
||||
* @since 0.2
|
||||
*/
|
||||
class AMP_DOM_Utils
|
||||
{
|
||||
/**
|
||||
* Regular expression pattern to match events and actions within an 'on' attribute.
|
||||
*
|
||||
* @since 1.4.0
|
||||
* @var string
|
||||
*/
|
||||
const AMP_EVENT_ACTIONS_REGEX_PATTERN = '/((?<event>[^:;]+):(?<actions>(?:[^;,\\(]+(?:\\([^\\)]+\\))?,?)+))+?/';
|
||||
/**
|
||||
* Regular expression pattern to match individual actions within an event.
|
||||
*
|
||||
* @since 1.4.0
|
||||
* @var string
|
||||
*/
|
||||
const AMP_ACTION_REGEX_PATTERN = '/(?<action>[^(),\\s]+(?:\\([^\\)]+\\))?)+/';
|
||||
/**
|
||||
* Regular expression pattern to match the contents of the <body> element.
|
||||
*
|
||||
* @since 1.5.0
|
||||
* @var string
|
||||
*/
|
||||
const HTML_BODY_CONTENTS_PATTERN = '#^.*?<body.*?>(.*)</body>.*?$#si';
|
||||
/**
|
||||
* Return a valid Dom\Document representing HTML document passed as a parameter.
|
||||
*
|
||||
* @since 0.7
|
||||
* @see AMP_DOM_Utils::get_content_from_dom_node()
|
||||
* @codeCoverageIgnore
|
||||
* @deprecated Use AmpProject\Dom\Document::fromHtml( $html, $encoding ) instead.
|
||||
*
|
||||
* @param string $document Valid HTML document to be represented by a Dom\Document.
|
||||
* @param string $encoding Optional. Encoding to use for the content.
|
||||
* @return Document|false Returns Dom\Document, or false if conversion failed.
|
||||
*/
|
||||
public static function get_dom($document, $encoding = null)
|
||||
{
|
||||
\_deprecated_function(__METHOD__, '1.5.0', 'AmpProject\\Dom\\Document::fromHtml()');
|
||||
$options = Options::DEFAULTS;
|
||||
if (null !== $encoding) {
|
||||
$options[Document\Option::ENCODING] = $encoding;
|
||||
}
|
||||
return Document::fromHtml($document, $options);
|
||||
}
|
||||
/**
|
||||
* Determine whether a node can be in the head.
|
||||
*
|
||||
* @link https://github.com/ampproject/amphtml/blob/445d6e3be8a5063e2738c6f90fdcd57f2b6208be/validator/engine/htmlparser.js#L83-L100
|
||||
* @link https://www.w3.org/TR/html5/document-metadata.html
|
||||
* @codeCoverageIgnore
|
||||
* @deprecated Use AmpProject\Dom\Document->isValidHeadNode() instead.
|
||||
*
|
||||
* @param DOMNode $node Node.
|
||||
* @return bool Whether valid head node.
|
||||
*/
|
||||
public static function is_valid_head_node(\DOMNode $node)
|
||||
{
|
||||
\_deprecated_function(__METHOD__, '1.5.0', 'AmpProject\\Dom\\Document->isValidHeadNode()');
|
||||
return Document::fromNode($node)->isValidHeadNode($node);
|
||||
}
|
||||
/**
|
||||
* Return a valid Dom\Document representing arbitrary HTML content passed as a parameter.
|
||||
*
|
||||
* @see Reciprocal function get_content_from_dom()
|
||||
*
|
||||
* @since 0.2
|
||||
*
|
||||
* @param string $content Valid HTML content to be represented by a Dom\Document.
|
||||
* @param string $encoding Optional. Encoding to use for the content. Defaults to `get_bloginfo( 'charset' )`.
|
||||
*
|
||||
* @return Document|false Returns a DOM document, or false if conversion failed.
|
||||
*/
|
||||
public static function get_dom_from_content($content, $encoding = null)
|
||||
{
|
||||
// Detect encoding from the current WordPress installation.
|
||||
if (null === $encoding) {
|
||||
$encoding = \get_bloginfo('charset');
|
||||
}
|
||||
/*
|
||||
* Wrap in dummy tags, since XML needs one parent node.
|
||||
* It also makes it easier to loop through nodes.
|
||||
* We can later use this to extract our nodes.
|
||||
*/
|
||||
$document = "<html><head></head><body>{$content}</body></html>";
|
||||
$options = Options::DEFAULTS;
|
||||
$options[Document\Option::ENCODING] = $encoding;
|
||||
return Document::fromHtml($document, $options);
|
||||
}
|
||||
/**
|
||||
* Return valid HTML *body* content extracted from the Dom\Document passed as a parameter.
|
||||
*
|
||||
* @since 0.2
|
||||
* @see AMP_DOM_Utils::get_content_from_dom_node() Reciprocal function.
|
||||
*
|
||||
* @param Document $dom Represents an HTML document from which to extract HTML content.
|
||||
* @return string Returns the HTML content of the body element represented in the Dom\Document.
|
||||
*/
|
||||
public static function get_content_from_dom(Document $dom)
|
||||
{
|
||||
return \preg_replace(static::HTML_BODY_CONTENTS_PATTERN, '$1', $dom->saveHTML($dom->body));
|
||||
}
|
||||
/**
|
||||
* Return valid HTML content extracted from the DOMNode passed as a parameter.
|
||||
*
|
||||
* @since 0.6
|
||||
* @see AMP_DOM_Utils::get_dom() Where the operations in this method are mirrored.
|
||||
* @see AMP_DOM_Utils::get_content_from_dom() Reciprocal function.
|
||||
* @codeCoverageIgnore
|
||||
* @deprecated Use Dom\Document->saveHtml( $node ) instead.
|
||||
*
|
||||
* @param Document $dom Represents an HTML document.
|
||||
* @param DOMElement $node Represents an HTML element of the $dom from which to extract HTML content.
|
||||
* @return string Returns the HTML content represented in the DOMNode
|
||||
*/
|
||||
public static function get_content_from_dom_node(Document $dom, $node)
|
||||
{
|
||||
\_deprecated_function(__METHOD__, '1.5.0', 'AmpProject\\Dom\\Document::saveHtml()');
|
||||
return $dom->saveHTML($node);
|
||||
}
|
||||
/**
|
||||
* Create a new node w/attributes (a DOMElement) and add to the passed Dom\Document.
|
||||
*
|
||||
* @since 0.2
|
||||
*
|
||||
* @param Document $dom A representation of an HTML document to add the new node to.
|
||||
* @param string $tag A valid HTML element tag for the element to be added.
|
||||
* @param string[] $attributes One of more valid attributes for the new node.
|
||||
*
|
||||
* @return Element|false The element for the given $tag, or false on failure
|
||||
*/
|
||||
public static function create_node(Document $dom, $tag, $attributes)
|
||||
{
|
||||
$node = $dom->createElement($tag);
|
||||
self::add_attributes_to_node($node, $attributes);
|
||||
return $node;
|
||||
}
|
||||
/**
|
||||
* Extract a DOMElement node's HTML element attributes and return as an array.
|
||||
*
|
||||
* @since 0.2
|
||||
*
|
||||
* @param DOMElement $node Represents an HTML element for which to extract attributes.
|
||||
*
|
||||
* @return string[] The attributes for the passed node, or an
|
||||
* empty array if it has no attributes.
|
||||
*/
|
||||
public static function get_node_attributes_as_assoc_array($node)
|
||||
{
|
||||
$attributes = [];
|
||||
if (!$node->hasAttributes()) {
|
||||
return $attributes;
|
||||
}
|
||||
foreach ($node->attributes as $attribute) {
|
||||
$attributes[$attribute->nodeName] = $attribute->nodeValue;
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
/**
|
||||
* Add one or more HTML element attributes to a node's DOMElement.
|
||||
*
|
||||
* @since 0.2
|
||||
*
|
||||
* @param DOMElement $node Represents an HTML element.
|
||||
* @param string[] $attributes One or more attributes for the node's HTML element.
|
||||
*/
|
||||
public static function add_attributes_to_node($node, $attributes)
|
||||
{
|
||||
foreach ($attributes as $name => $value) {
|
||||
try {
|
||||
$node->setAttribute($name, $value);
|
||||
} catch (\DOMException $e) {
|
||||
/*
|
||||
* Catch a "Invalid Character Error" when libxml is able to parse attributes with invalid characters,
|
||||
* but it throws error when attempting to set them via DOM methods. For example, '...this' can be parsed
|
||||
* as an attribute but it will throw an exception when attempting to setAttribute().
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Determines if a DOMElement's node is empty or not..
|
||||
*
|
||||
* @since 0.2
|
||||
*
|
||||
* @param DOMElement $node Represents an HTML element.
|
||||
* @return bool Returns true if the DOMElement has no child nodes and
|
||||
* the textContent property of the DOMElement is empty;
|
||||
* Otherwise it returns false.
|
||||
*/
|
||||
public static function is_node_empty($node)
|
||||
{
|
||||
return \false === $node->hasChildNodes() && empty($node->textContent);
|
||||
}
|
||||
/**
|
||||
* Forces HTML element closing tags given a Dom\Document and optional DOMElement.
|
||||
*
|
||||
* @since 0.2
|
||||
* @codeCoverageIgnore
|
||||
* @deprecated
|
||||
* @internal
|
||||
*
|
||||
* @param Document $dom Represents HTML document on which to force closing tags.
|
||||
* @param DOMElement $node Represents HTML element to start closing tags on.
|
||||
* If not passed, defaults to first child of body.
|
||||
*/
|
||||
public static function recursive_force_closing_tags($dom, $node = null)
|
||||
{
|
||||
\_deprecated_function(__METHOD__, '0.7');
|
||||
if (null === $node) {
|
||||
$node = $dom->body;
|
||||
}
|
||||
if (\XML_ELEMENT_NODE !== $node->nodeType) {
|
||||
return;
|
||||
}
|
||||
if (self::is_self_closing_tag($node->nodeName)) {
|
||||
/*
|
||||
* Ensure there is no text content to accidentally force a child
|
||||
*/
|
||||
$node->textContent = '';
|
||||
return;
|
||||
}
|
||||
if (self::is_node_empty($node)) {
|
||||
$text_node = $dom->createTextNode('');
|
||||
$node->appendChild($text_node);
|
||||
return;
|
||||
}
|
||||
$num_children = $node->childNodes->length;
|
||||
for ($i = $num_children - 1; $i >= 0; $i--) {
|
||||
$child = $node->childNodes->item($i);
|
||||
self::recursive_force_closing_tags($dom, $child);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Determines if an HTML element tag is validly a self-closing tag per W3C HTML5 specs.
|
||||
*
|
||||
* @since 0.2
|
||||
*
|
||||
* @param string $tag Tag.
|
||||
* @return bool Returns true if a valid self-closing tag, false if not.
|
||||
*/
|
||||
private static function is_self_closing_tag($tag)
|
||||
{
|
||||
return \in_array(\strtolower($tag), Tag::SELF_CLOSING_TAGS, \true);
|
||||
}
|
||||
/**
|
||||
* Check whether a given element has a specific class.
|
||||
*
|
||||
* @since 1.4.0
|
||||
*
|
||||
* @param DOMElement $element Element to check the classes of.
|
||||
* @param string $class Class to check for.
|
||||
* @return bool Whether the element has the requested class.
|
||||
*/
|
||||
public static function has_class(\DOMElement $element, $class)
|
||||
{
|
||||
if (!$element->hasAttribute('class')) {
|
||||
return \false;
|
||||
}
|
||||
$classes = $element->getAttribute('class');
|
||||
return \in_array($class, \preg_split('/\\s/', $classes), \true);
|
||||
}
|
||||
/**
|
||||
* Get the ID for an element.
|
||||
*
|
||||
* If the element does not have an ID, create one first.
|
||||
*
|
||||
* @since 1.4.0
|
||||
* @since 1.5.1 Deprecated for AmpProject\Dom\Document::getElementId()
|
||||
*
|
||||
* @deprecated Use AmpProject\Dom\Document::getElementId() instead.
|
||||
*
|
||||
* @param DOMElement|Element $element Element to get the ID for.
|
||||
* @param string $prefix Optional. Defaults to 'amp-wp-id'.
|
||||
* @return string ID to use.
|
||||
*/
|
||||
public static function get_element_id($element, $prefix = 'amp-wp-id')
|
||||
{
|
||||
\_deprecated_function('AMP_DOM_Utils::get_element_id', '1.5.1', 'Use AmpProject\\Amp\\Dom\\Document::getElementId() instead');
|
||||
return Document::fromNode($element)->getElementId($element, $prefix);
|
||||
}
|
||||
/**
|
||||
* Register an AMP action to an event on a given element.
|
||||
*
|
||||
* If the element already contains one or more events or actions, the method
|
||||
* will assemble them in a smart way.
|
||||
*
|
||||
* @since 1.4.0
|
||||
*
|
||||
* @param DOMElement $element Element to add an action to.
|
||||
* @param string $event Event to trigger the action on.
|
||||
* @param string $action Action to add.
|
||||
*/
|
||||
public static function add_amp_action(\DOMElement $element, $event, $action)
|
||||
{
|
||||
$event_action_string = "{$event}:{$action}";
|
||||
if (!$element->hasAttribute('on')) {
|
||||
// There's no "on" attribute yet, so just add it and be done.
|
||||
$element->setAttribute('on', $event_action_string);
|
||||
return;
|
||||
}
|
||||
$element->setAttribute('on', self::merge_amp_actions($element->getAttribute('on'), $event_action_string));
|
||||
}
|
||||
/**
|
||||
* Merge two sets of AMP events & actions.
|
||||
*
|
||||
* @since 1.4.0
|
||||
*
|
||||
* @param string $first First event/action string.
|
||||
* @param string $second First event/action string.
|
||||
* @return string Merged event/action string.
|
||||
*/
|
||||
public static function merge_amp_actions($first, $second)
|
||||
{
|
||||
$events = [];
|
||||
foreach ([$first, $second] as $event_action_string) {
|
||||
$matches = [];
|
||||
$results = \preg_match_all(self::AMP_EVENT_ACTIONS_REGEX_PATTERN, $event_action_string, $matches);
|
||||
if (!$results || !isset($matches['event'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($matches['event'] as $index => $event) {
|
||||
$events[$event][] = $matches['actions'][$index];
|
||||
}
|
||||
}
|
||||
$value_strings = [];
|
||||
foreach ($events as $event => $action_strings_array) {
|
||||
$actions_array = [];
|
||||
\array_walk($action_strings_array, static function ($actions) use(&$actions_array) {
|
||||
$matches = [];
|
||||
$results = \preg_match_all(self::AMP_ACTION_REGEX_PATTERN, $actions, $matches);
|
||||
if (!$results || !isset($matches['action'])) {
|
||||
$actions_array[] = $actions;
|
||||
return;
|
||||
}
|
||||
$actions_array = \array_merge($actions_array, $matches['action']);
|
||||
});
|
||||
$actions = \implode(',', \array_unique(\array_filter($actions_array)));
|
||||
$value_strings[] = "{$event}:{$actions}";
|
||||
}
|
||||
return \implode(';', $value_strings);
|
||||
}
|
||||
/**
|
||||
* Copy one or more attributes from one element to the other.
|
||||
*
|
||||
* @param array|string $attributes Attribute name or array of attribute names to copy.
|
||||
* @param DOMElement $from DOM element to copy the attributes from.
|
||||
* @param DOMElement $to DOM element to copy the attributes to.
|
||||
* @param string $default_separator Default separator to use for multiple values if the attribute is not known.
|
||||
*/
|
||||
public static function copy_attributes($attributes, \DOMElement $from, \DOMElement $to, $default_separator = ',')
|
||||
{
|
||||
foreach ((array) $attributes as $attribute) {
|
||||
if ($from->hasAttribute($attribute)) {
|
||||
$values = $from->getAttribute($attribute);
|
||||
if ($to->hasAttribute($attribute)) {
|
||||
switch ($attribute) {
|
||||
case 'on':
|
||||
$values = self::merge_amp_actions($to->getAttribute($attribute), $values);
|
||||
break;
|
||||
case 'class':
|
||||
$values = $to->getAttribute($attribute) . ' ' . $values;
|
||||
break;
|
||||
default:
|
||||
$values = $to->getAttribute($attribute) . $default_separator . $values;
|
||||
}
|
||||
}
|
||||
$to->setAttribute($attribute, $values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,351 @@
|
||||
<?php
|
||||
|
||||
namespace Google\Web_Stories_Dependencies;
|
||||
|
||||
/**
|
||||
* Class AMP_Image_Dimension_Extractor
|
||||
*
|
||||
* @package AMP
|
||||
*/
|
||||
/**
|
||||
* Class with static methods to extract image dimensions.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class AMP_Image_Dimension_Extractor
|
||||
{
|
||||
const STATUS_FAILED_LAST_ATTEMPT = 'failed';
|
||||
const STATUS_IMAGE_EXTRACTION_FAILED = 'failed';
|
||||
/**
|
||||
* Internal flag whether callbacks have been registered.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $callbacks_registered = \false;
|
||||
/**
|
||||
* Extracts dimensions from image URLs.
|
||||
*
|
||||
* @since 0.2
|
||||
*
|
||||
* @param array|string $urls Array of URLs to extract dimensions from, or a single URL string.
|
||||
* @return array|string Extracted dimensions keyed by original URL, or else the single set of dimensions if one URL string is passed.
|
||||
*/
|
||||
public static function extract($urls)
|
||||
{
|
||||
if (!self::$callbacks_registered) {
|
||||
self::register_callbacks();
|
||||
}
|
||||
$return_dimensions = [];
|
||||
// Back-compat for users calling this method directly.
|
||||
$is_single = \is_string($urls);
|
||||
if ($is_single) {
|
||||
$urls = [$urls];
|
||||
}
|
||||
// Normalize URLs and also track a map of normalized-to-original as we'll need it to reformat things when returning the data.
|
||||
$url_map = [];
|
||||
$normalized_urls = [];
|
||||
foreach ($urls as $original_url) {
|
||||
$normalized_url = self::normalize_url($original_url);
|
||||
if (\false !== $normalized_url) {
|
||||
$url_map[$original_url] = $normalized_url;
|
||||
$normalized_urls[] = $normalized_url;
|
||||
} else {
|
||||
// This is not a URL we can extract dimensions from, so default to false.
|
||||
$return_dimensions[$original_url] = \false;
|
||||
}
|
||||
}
|
||||
$extracted_dimensions = \array_fill_keys($normalized_urls, \false);
|
||||
/**
|
||||
* Filters the dimensions extracted from image URLs.
|
||||
*
|
||||
* @since 0.5.1
|
||||
*
|
||||
* @param array $extracted_dimensions Extracted dimensions, initially mapping images URLs to false.
|
||||
*/
|
||||
$extracted_dimensions = \apply_filters('amp_extract_image_dimensions_batch', $extracted_dimensions);
|
||||
// We need to return a map with the original (un-normalized URL) as we that to match nodes that need dimensions.
|
||||
foreach ($url_map as $original_url => $normalized_url) {
|
||||
$return_dimensions[$original_url] = $extracted_dimensions[$normalized_url];
|
||||
}
|
||||
// Back-compat: just return the dimensions, not the full mapped array.
|
||||
if ($is_single) {
|
||||
return \current($return_dimensions);
|
||||
}
|
||||
return $return_dimensions;
|
||||
}
|
||||
/**
|
||||
* Normalizes the given URL.
|
||||
*
|
||||
* This method ensures the URL has a scheme and, if relative, is prepended the WordPress site URL.
|
||||
*
|
||||
* @param string $url URL to normalize.
|
||||
* @return string|false Normalized URL, or false if normalization failed.
|
||||
*/
|
||||
public static function normalize_url($url)
|
||||
{
|
||||
if (empty($url)) {
|
||||
return \false;
|
||||
}
|
||||
if (0 === \strpos($url, 'data:')) {
|
||||
return \false;
|
||||
}
|
||||
$normalized_url = $url;
|
||||
if (0 === \strpos($url, '//')) {
|
||||
$normalized_url = \set_url_scheme($url, 'http');
|
||||
} else {
|
||||
$parsed = \wp_parse_url($url);
|
||||
if (!isset($parsed['host'])) {
|
||||
$path = '';
|
||||
if (isset($parsed['path'])) {
|
||||
$path .= $parsed['path'];
|
||||
}
|
||||
if (isset($parsed['query'])) {
|
||||
$path .= '?' . $parsed['query'];
|
||||
}
|
||||
$home = \home_url();
|
||||
$home_path = \wp_parse_url($home, \PHP_URL_PATH);
|
||||
if (!empty($home_path)) {
|
||||
$home = \substr($home, 0, -\strlen($home_path));
|
||||
}
|
||||
$normalized_url = $home . $path;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Apply filters on the normalized image URL for dimension extraction.
|
||||
*
|
||||
* @since 1.1
|
||||
*
|
||||
* @param string $normalized_url Normalized image URL.
|
||||
* @param string $url Original image URL.
|
||||
*/
|
||||
$normalized_url = \apply_filters('amp_normalized_dimension_extractor_image_url', $normalized_url, $url);
|
||||
return $normalized_url;
|
||||
}
|
||||
/**
|
||||
* Registers the necessary callbacks.
|
||||
*/
|
||||
private static function register_callbacks()
|
||||
{
|
||||
self::$callbacks_registered = \true;
|
||||
\add_filter('amp_extract_image_dimensions_batch', [__CLASS__, 'extract_by_filename_or_filesystem'], 100);
|
||||
\add_filter('amp_extract_image_dimensions_batch', [__CLASS__, 'extract_by_downloading_images'], 999, 1);
|
||||
/**
|
||||
* Fires after the amp_extract_image_dimensions_batch filter has been added to extract by downloading images.
|
||||
*
|
||||
* @since 0.5.1
|
||||
*/
|
||||
\do_action('amp_extract_image_dimensions_batch_callbacks_registered');
|
||||
}
|
||||
/**
|
||||
* Extract dimensions from filename if dimension exists or from file system.
|
||||
*
|
||||
* @param array $dimensions Image urls mapped to dimensions.
|
||||
* @return array Dimensions mapped to image urls, or false if they could not be retrieved
|
||||
*/
|
||||
public static function extract_by_filename_or_filesystem($dimensions)
|
||||
{
|
||||
if (empty($dimensions) || !\is_array($dimensions)) {
|
||||
return [];
|
||||
}
|
||||
$using_ext_object_cache = \wp_using_ext_object_cache();
|
||||
$ext_types = \wp_get_ext_types();
|
||||
if (empty($ext_types['image'])) {
|
||||
return $dimensions;
|
||||
}
|
||||
$image_ext_types = $ext_types['image'];
|
||||
unset($ext_types);
|
||||
$upload_dir = \wp_get_upload_dir();
|
||||
$base_upload_uri = \trailingslashit($upload_dir['baseurl']);
|
||||
$base_upload_dir = \trailingslashit($upload_dir['basedir']);
|
||||
foreach ($dimensions as $url => $value) {
|
||||
// Check whether some other callback attached to the filter already provided dimensions for this image.
|
||||
if (!empty($value) && \is_array($value)) {
|
||||
continue;
|
||||
}
|
||||
$url_without_query_fragment = \strtok($url, '?#');
|
||||
// Parse info out of the URL, including the file extension and (optionally) the dimensions.
|
||||
if (!\preg_match('/(?:-(?<width>[1-9][0-9]*)x(?<height>[1-9][0-9]*))?\\.(?<ext>\\w+)$/', $url_without_query_fragment, $matches)) {
|
||||
continue;
|
||||
}
|
||||
// Skip images don't have recognized extensions.
|
||||
if (!\in_array(\strtolower($matches['ext']), $image_ext_types, \true)) {
|
||||
continue;
|
||||
}
|
||||
// Use image dimension from the file name.
|
||||
if (!empty($matches['width']) && !empty($matches['height'])) {
|
||||
$dimensions[$url] = ['width' => (int) $matches['width'], 'height' => (int) $matches['height']];
|
||||
continue;
|
||||
}
|
||||
// Verify that the URL is for an uploaded file.
|
||||
if (0 !== \strpos($url_without_query_fragment, $base_upload_uri)) {
|
||||
continue;
|
||||
}
|
||||
$upload_relative_path = \substr($url_without_query_fragment, \strlen($base_upload_uri));
|
||||
// Bail if the URL contains relative paths.
|
||||
if (0 !== \validate_file($upload_relative_path)) {
|
||||
continue;
|
||||
}
|
||||
// Get image dimension from file system.
|
||||
$image_file = $base_upload_dir . $upload_relative_path;
|
||||
$image_size = [];
|
||||
list($transient_name) = self::get_transient_names($url);
|
||||
// When using an external object cache, try to first see if dimensions have already been obtained. This is
|
||||
// not done for a non-external object cache (i.e. when wp_options is used for transients) because then
|
||||
// we are not storing the dimensions in a transient, because it is more performant to read the dimensions
|
||||
// from the filesystem--both in terms of time and storage--than to store dimensions in wp_options.
|
||||
if ($using_ext_object_cache) {
|
||||
$image_size = \get_transient($transient_name);
|
||||
$image_size = !empty($image_size) && \is_array($image_size) ? $image_size : [];
|
||||
}
|
||||
if (empty($image_size) && \file_exists($image_file)) {
|
||||
if (\function_exists('wp_getimagesize')) {
|
||||
$image_size = \wp_getimagesize($image_file);
|
||||
} elseif (\function_exists('getimagesize')) {
|
||||
$image_size = @\getimagesize($image_file);
|
||||
// phpcs:ignore WordPress.PHP.NoSilencedErrors
|
||||
}
|
||||
if ($using_ext_object_cache && !empty($image_size) && \is_array($image_size)) {
|
||||
\set_transient($transient_name, $image_size);
|
||||
}
|
||||
}
|
||||
if (!empty($image_size) && \is_array($image_size)) {
|
||||
$dimensions[$url] = ['width' => (int) $image_size[0], 'height' => (int) $image_size[1]];
|
||||
}
|
||||
}
|
||||
return $dimensions;
|
||||
}
|
||||
/**
|
||||
* Get transient names.
|
||||
*
|
||||
* @param string $url Image URL.
|
||||
* @return array {
|
||||
* @type string $0 Transient name for storing dimensions.
|
||||
* @type string $1 Transient name for image fetching lock.
|
||||
* }
|
||||
*/
|
||||
private static function get_transient_names($url)
|
||||
{
|
||||
$url_hash = \md5($url);
|
||||
return [\sprintf('amp_img_%s', $url_hash), \sprintf('amp_lock_%s', $url_hash)];
|
||||
}
|
||||
/**
|
||||
* Extract dimensions from downloaded images (or transient/cached dimensions from downloaded images)
|
||||
*
|
||||
* @param array $dimensions Image urls mapped to dimensions.
|
||||
* @param bool $mode Deprecated.
|
||||
* @return array Dimensions mapped to image urls, or false if they could not be retrieved
|
||||
*/
|
||||
public static function extract_by_downloading_images($dimensions, $mode = \false)
|
||||
{
|
||||
if ($mode) {
|
||||
\_deprecated_argument(__METHOD__, 'AMP 1.1');
|
||||
}
|
||||
$transient_expiration = 30 * \DAY_IN_SECONDS;
|
||||
$urls_to_fetch = [];
|
||||
$images = [];
|
||||
self::determine_which_images_to_fetch($dimensions, $urls_to_fetch);
|
||||
try {
|
||||
self::fetch_images($urls_to_fetch, $images);
|
||||
self::process_fetched_images($urls_to_fetch, $images, $dimensions, $transient_expiration);
|
||||
} catch (\Exception $exception) {
|
||||
\trigger_error(\esc_html($exception->getMessage()), \E_USER_WARNING);
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
}
|
||||
return $dimensions;
|
||||
}
|
||||
/**
|
||||
* Determine which images to fetch by checking for dimensions in transient/cache.
|
||||
* Creates a short lived transient that acts as a semaphore so that another visitor
|
||||
* doesn't trigger a remote fetch for the same image at the same time.
|
||||
*
|
||||
* @param array $dimensions Image urls mapped to dimensions.
|
||||
* @param array $urls_to_fetch Urls of images to fetch because dimensions are not in transient/cache.
|
||||
*/
|
||||
private static function determine_which_images_to_fetch(&$dimensions, &$urls_to_fetch)
|
||||
{
|
||||
foreach ($dimensions as $url => $value) {
|
||||
// Check whether some other callback attached to the filter already provided dimensions for this image.
|
||||
if (\is_array($value) || empty($url)) {
|
||||
continue;
|
||||
}
|
||||
list($transient_name, $transient_lock_name) = self::get_transient_names($url);
|
||||
$cached_dimensions = \get_transient($transient_name);
|
||||
// If we're able to retrieve the dimensions from a transient, set them and move on.
|
||||
if (\is_array($cached_dimensions)) {
|
||||
$dimensions[$url] = ['width' => $cached_dimensions[0], 'height' => $cached_dimensions[1]];
|
||||
continue;
|
||||
}
|
||||
// If the value in the transient reflects we couldn't get dimensions for this image the last time we tried, move on.
|
||||
if (self::STATUS_FAILED_LAST_ATTEMPT === $cached_dimensions) {
|
||||
$dimensions[$url] = \false;
|
||||
continue;
|
||||
}
|
||||
// If somebody is already trying to extract dimensions for this transient right now, move on.
|
||||
if (\false !== \get_transient($transient_lock_name)) {
|
||||
$dimensions[$url] = \false;
|
||||
continue;
|
||||
}
|
||||
// Include the image as a url to fetch.
|
||||
$urls_to_fetch[$url] = [];
|
||||
$urls_to_fetch[$url]['url'] = $url;
|
||||
$urls_to_fetch[$url]['transient_name'] = $transient_name;
|
||||
$urls_to_fetch[$url]['transient_lock_name'] = $transient_lock_name;
|
||||
\set_transient($transient_lock_name, 1, \MINUTE_IN_SECONDS);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Fetch dimensions of remote images
|
||||
*
|
||||
* @throws Exception When cURL handle cannot be added.
|
||||
*
|
||||
* @param array $urls_to_fetch Image src urls to fetch.
|
||||
* @param array $images Array to populate with results of image/dimension inspection.
|
||||
*/
|
||||
private static function fetch_images($urls_to_fetch, &$images)
|
||||
{
|
||||
$urls = \array_keys($urls_to_fetch);
|
||||
$client = new \Google\Web_Stories_Dependencies\FasterImage\FasterImage();
|
||||
/**
|
||||
* Filters the user agent for obtaining the image dimensions.
|
||||
*
|
||||
* @param string $user_agent User agent.
|
||||
*/
|
||||
$client->setUserAgent(\apply_filters('amp_extract_image_dimensions_get_user_agent', self::get_default_user_agent()));
|
||||
$client->setBufferSize(1024);
|
||||
$client->setSslVerifyHost(\true);
|
||||
$client->setSslVerifyPeer(\true);
|
||||
$images = $client->batch($urls);
|
||||
}
|
||||
/**
|
||||
* Determine success or failure of remote fetch, integrate fetched dimensions into url to dimension mapping,
|
||||
* cache fetched dimensions via transient and release/delete semaphore transient
|
||||
*
|
||||
* @param array $urls_to_fetch List of image urls that were fetched and transient names corresponding to each (for unlocking semaphore, setting "real" transient).
|
||||
* @param array $images Results of remote fetch mapping fetched image url to dimensions.
|
||||
* @param array $dimensions Map of image url to dimensions to be updated with results of remote fetch.
|
||||
* @param int $transient_expiration Duration image dimensions should exist in transient/cache.
|
||||
*/
|
||||
private static function process_fetched_images($urls_to_fetch, $images, &$dimensions, $transient_expiration)
|
||||
{
|
||||
foreach ($urls_to_fetch as $url_data) {
|
||||
$image_data = $images[$url_data['url']];
|
||||
if (self::STATUS_IMAGE_EXTRACTION_FAILED === $image_data['size']) {
|
||||
$dimensions[$url_data['url']] = \false;
|
||||
\set_transient($url_data['transient_name'], self::STATUS_FAILED_LAST_ATTEMPT, $transient_expiration);
|
||||
} else {
|
||||
$dimensions[$url_data['url']] = ['width' => $image_data['size'][0], 'height' => $image_data['size'][1]];
|
||||
\set_transient($url_data['transient_name'], [$image_data['size'][0], $image_data['size'][1]], $transient_expiration);
|
||||
}
|
||||
\delete_transient($url_data['transient_lock_name']);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get default user agent
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_default_user_agent()
|
||||
{
|
||||
return 'amp-wp, v' . \AMP__VERSION . ', ' . \home_url();
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Google\Web_Stories_Dependencies;
|
||||
|
||||
/**
|
||||
* Class AMP_String_Utils
|
||||
*
|
||||
* @package AMP
|
||||
*/
|
||||
/**
|
||||
* Class with static string utility methods.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class AMP_String_Utils
|
||||
{
|
||||
/**
|
||||
* Checks whether a given string ends in the given substring.
|
||||
*
|
||||
* @param string $haystack Input string.
|
||||
* @param string $needle Substring to look for at the end of $haystack.
|
||||
* @return bool True if $haystack ends in $needle, false otherwise.
|
||||
*/
|
||||
public static function endswith($haystack, $needle)
|
||||
{
|
||||
return '' !== $haystack && '' !== $needle && \substr($haystack, -\strlen($needle)) === $needle;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class CachedRemoteGetRequest.
|
||||
*
|
||||
* @package AmpProject\AmpWP
|
||||
*/
|
||||
namespace Google\Web_Stories_Dependencies\AmpProject\AmpWP\RemoteRequest;
|
||||
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Exception\FailedToGetCachedResponse;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Exception\FailedToGetFromRemoteUrl;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\RemoteGetRequest;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\RemoteRequest\RemoteGetRequestResponse;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Response;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
/**
|
||||
* Caching decorator for RemoteGetRequest implementations.
|
||||
*
|
||||
* Caching uses WordPress transients.
|
||||
*
|
||||
* @package AmpProject\AmpWP
|
||||
* @since 2.0
|
||||
* @internal
|
||||
*/
|
||||
final class CachedRemoteGetRequest implements RemoteGetRequest
|
||||
{
|
||||
/**
|
||||
* Prefix to use to identify transients.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TRANSIENT_PREFIX = 'amp_remote_request_';
|
||||
/**
|
||||
* Cache control header directive name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const CACHE_CONTROL = 'Cache-Control';
|
||||
/**
|
||||
* Remote request object to decorate with caching.
|
||||
*
|
||||
* @var RemoteGetRequest
|
||||
*/
|
||||
private $remote_request;
|
||||
/**
|
||||
* Cache expiration time in seconds.
|
||||
*
|
||||
* This will be used by default for successful requests when the 'cache-control: max-age' was not provided.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $expiry;
|
||||
/**
|
||||
* Minimum cache expiration time in seconds.
|
||||
*
|
||||
* This will be used for failed requests, or for successful requests when the 'cache-control: max-age' is inferior.
|
||||
* Caching will never expire quicker than this minimum.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $min_expiry;
|
||||
/**
|
||||
* Whether to use Cache-Control headers to decide on expiry times if available.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $use_cache_control;
|
||||
/**
|
||||
* Instantiate a CachedRemoteGetRequest object.
|
||||
*
|
||||
* This is a decorator that can wrap around an existing remote request object to add a caching layer.
|
||||
*
|
||||
* @param RemoteGetRequest $remote_request Remote request object to decorate with caching.
|
||||
* @param int|float $expiry Optional. Default cache expiry in seconds. Defaults to 30 days.
|
||||
* @param int|float $min_expiry Optional. Default enforced minimum cache expiry in seconds. Defaults
|
||||
* to 24 hours.
|
||||
* @param bool $use_cache_control Optional. Use Cache-Control headers for expiry if available. Defaults
|
||||
* to true.
|
||||
*/
|
||||
public function __construct(RemoteGetRequest $remote_request, $expiry = \MONTH_IN_SECONDS, $min_expiry = \DAY_IN_SECONDS, $use_cache_control = \true)
|
||||
{
|
||||
$this->remote_request = $remote_request;
|
||||
$this->expiry = (int) $expiry;
|
||||
$this->min_expiry = (int) $min_expiry;
|
||||
$this->use_cache_control = (bool) $use_cache_control;
|
||||
}
|
||||
/**
|
||||
* Do a GET request to retrieve the contents of a remote URL.
|
||||
*
|
||||
* @todo Should this also respect additional Cache-Control directives like 'no-cache'?
|
||||
*
|
||||
* @param string $url URL to get.
|
||||
* @param array $headers Optional. Associative array of headers to send with the request. Defaults to empty array.
|
||||
* @return Response Response for the executed request.
|
||||
* @throws FailedToGetCachedResponse If retrieving the contents from the URL failed.
|
||||
*/
|
||||
public function get($url, $headers = [])
|
||||
{
|
||||
$cache_key = self::TRANSIENT_PREFIX . \md5(__CLASS__ . $url);
|
||||
$cached_response = \get_transient($cache_key);
|
||||
$headers = [];
|
||||
if (\is_string($cached_response)) {
|
||||
if (\PHP_MAJOR_VERSION >= 7) {
|
||||
$cached_response = \unserialize($cached_response, [CachedResponse::class, DateTimeImmutable::class]);
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize,PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound
|
||||
} else {
|
||||
// PHP 5.6 does not provide the second $options argument yet.
|
||||
$cached_response = \unserialize($cached_response);
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
|
||||
}
|
||||
}
|
||||
if (!$cached_response instanceof CachedResponse || $cached_response->is_expired()) {
|
||||
try {
|
||||
$response = $this->remote_request->get($url, $headers);
|
||||
$status = $response->getStatusCode();
|
||||
/** @var DateTimeImmutable $expiry */
|
||||
$expiry = $this->get_expiry_time($response);
|
||||
$headers = $response->getHeaders();
|
||||
$body = $response->getBody();
|
||||
} catch (FailedToGetFromRemoteUrl $exception) {
|
||||
$status = $exception->getStatusCode();
|
||||
$expiry = new DateTimeImmutable("+ {$this->min_expiry} seconds");
|
||||
$body = $exception->getMessage();
|
||||
}
|
||||
$cached_response = new CachedResponse($body, $headers, $status, $expiry);
|
||||
// Transient extend beyond cache expiry to allow for serving stale content.
|
||||
// @TODO: We don't serve stale content atm, but rather synchronously refresh.
|
||||
// See https://github.com/ampproject/amp-wp/issues/5477.
|
||||
$transient_expiry = $expiry->modify("+ {$this->expiry} seconds");
|
||||
\set_transient($cache_key, \serialize($cached_response), $transient_expiry->getTimestamp());
|
||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
|
||||
}
|
||||
if (!$cached_response->is_valid()) {
|
||||
throw new FailedToGetCachedResponse($url);
|
||||
}
|
||||
return new RemoteGetRequestResponse($cached_response->get_body(), $cached_response->get_headers(), $cached_response->get_status_code());
|
||||
}
|
||||
/**
|
||||
* Get the expiry time of the data to cache.
|
||||
*
|
||||
* This will use the cache-control header information in the provided response or fall back to the provided default
|
||||
* expiry.
|
||||
*
|
||||
* @param Response $response Response object to get the expiry from.
|
||||
* @return DateTimeInterface Expiry of the data.
|
||||
*/
|
||||
private function get_expiry_time(Response $response)
|
||||
{
|
||||
if ($this->use_cache_control && $response->hasHeader(self::CACHE_CONTROL)) {
|
||||
$expiry = \max($this->min_expiry, $this->get_max_age($response->getHeader(self::CACHE_CONTROL)));
|
||||
return new DateTimeImmutable("+ {$expiry} seconds");
|
||||
}
|
||||
return new DateTimeImmutable("+ {$this->expiry} seconds");
|
||||
}
|
||||
/**
|
||||
* Get the max age setting from one or more cache-control header strings.
|
||||
*
|
||||
* @param array|string $cache_control_strings One or more cache control header strings.
|
||||
* @return int Value of the max-age cache directive. 0 if not found.
|
||||
*/
|
||||
private function get_max_age($cache_control_strings)
|
||||
{
|
||||
$max_age = 0;
|
||||
foreach ((array) $cache_control_strings as $cache_control_string) {
|
||||
$cache_control_parts = \array_map('trim', \explode(',', $cache_control_string));
|
||||
foreach ($cache_control_parts as $cache_control_part) {
|
||||
$cache_control_setting_parts = \array_map('trim', \explode('=', $cache_control_part));
|
||||
if (\count($cache_control_setting_parts) !== 2) {
|
||||
continue;
|
||||
}
|
||||
if ('max-age' === $cache_control_setting_parts[0]) {
|
||||
$max_age = \absint($cache_control_setting_parts[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $max_age;
|
||||
}
|
||||
}
|
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class CachedResponse.
|
||||
*
|
||||
* @package AmpProject\AmpWP
|
||||
*/
|
||||
namespace Google\Web_Stories_Dependencies\AmpProject\AmpWP\RemoteRequest;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
/**
|
||||
* Serializable object that represents a cached response together with its expiry time.
|
||||
*
|
||||
* @package AmpProject\AmpWP
|
||||
* @since 2.0
|
||||
* @internal
|
||||
*/
|
||||
final class CachedResponse
|
||||
{
|
||||
/**
|
||||
* Cached body.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $body;
|
||||
/**
|
||||
* Cached headers.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $headers;
|
||||
/**
|
||||
* Cached status code.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $status_code;
|
||||
/**
|
||||
* Expiry time of the cached value.
|
||||
*
|
||||
* @var DateTimeInterface
|
||||
*/
|
||||
private $expiry;
|
||||
/**
|
||||
* Instantiate a CachedResponse object.
|
||||
*
|
||||
* @param string $body Cached body.
|
||||
* @param string[] $headers Associative array of cached headers.
|
||||
* @param int $status_code Cached status code.
|
||||
* @param DateTimeInterface $expiry Expiry of the cached value.
|
||||
*/
|
||||
public function __construct($body, $headers, $status_code, DateTimeInterface $expiry)
|
||||
{
|
||||
$this->body = (string) $body;
|
||||
$this->headers = (array) $headers;
|
||||
$this->status_code = (int) $status_code;
|
||||
$this->expiry = $expiry;
|
||||
}
|
||||
/**
|
||||
* Get the cached body.
|
||||
*
|
||||
* @return string Cached body.
|
||||
*/
|
||||
public function get_body()
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
/**
|
||||
* Get the cached headers.
|
||||
*
|
||||
* @return string[] Cached headers.
|
||||
*/
|
||||
public function get_headers()
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
/**
|
||||
* Get the cached status code.
|
||||
*
|
||||
* @return int Cached status code.
|
||||
*/
|
||||
public function get_status_code()
|
||||
{
|
||||
return $this->status_code;
|
||||
}
|
||||
/**
|
||||
* Determine the validity of the cached response.
|
||||
*
|
||||
* @return bool Whether the cached response is valid.
|
||||
*/
|
||||
public function is_valid()
|
||||
{
|
||||
// Values are already typed, so we just control the status code for validity.
|
||||
return $this->status_code > 100 && $this->status_code <= 599;
|
||||
}
|
||||
/**
|
||||
* Get the expiry of the cached value.
|
||||
*
|
||||
* @return DateTimeInterface Expiry of the cached value.
|
||||
*/
|
||||
public function get_expiry()
|
||||
{
|
||||
return $this->expiry;
|
||||
}
|
||||
/**
|
||||
* Check whether the cached value is expired.
|
||||
*
|
||||
* @return bool Whether the cached value is expired.
|
||||
*/
|
||||
public function is_expired()
|
||||
{
|
||||
return new DateTimeImmutable('now') > $this->expiry;
|
||||
}
|
||||
}
|
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class WpHttpRemoteGetRequest.
|
||||
*
|
||||
* @package AmpProject\AmpWP
|
||||
*/
|
||||
namespace Google\Web_Stories_Dependencies\AmpProject\AmpWP\RemoteRequest;
|
||||
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Exception\FailedToGetFromRemoteUrl;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\RemoteGetRequest;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\RemoteRequest\RemoteGetRequestResponse;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Response;
|
||||
use Traversable;
|
||||
use WP_Error;
|
||||
use WP_Http;
|
||||
/**
|
||||
* Remote request transport using the WordPress WP_Http abstraction layer.
|
||||
*
|
||||
* @package AmpProject\AmpWP
|
||||
* @since 2.0
|
||||
* @internal
|
||||
*/
|
||||
final class WpHttpRemoteGetRequest implements RemoteGetRequest
|
||||
{
|
||||
/**
|
||||
* Default timeout value to use in seconds.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const DEFAULT_TIMEOUT = 5;
|
||||
/**
|
||||
* Default number of retry attempts to do.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const DEFAULT_RETRIES = 2;
|
||||
/**
|
||||
* List of HTTP status codes that are worth retrying for.
|
||||
*
|
||||
* @var int[]
|
||||
*/
|
||||
const RETRYABLE_STATUS_CODES = [WP_Http::REQUEST_TIMEOUT, WP_Http::LOCKED, WP_Http::TOO_MANY_REQUESTS, WP_Http::INTERNAL_SERVER_ERROR, WP_Http::SERVICE_UNAVAILABLE, WP_Http::GATEWAY_TIMEOUT];
|
||||
/**
|
||||
* Whether to verify SSL certificates or not.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private $ssl_verify;
|
||||
/**
|
||||
* Timeout value to use in seconds.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $timeout;
|
||||
/**
|
||||
* Number of retry attempts to do for an error that is worth retrying.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $retries;
|
||||
/**
|
||||
* Instantiate a WpHttpRemoteGetRequest object.
|
||||
*
|
||||
* @param bool $ssl_verify Optional. Whether to verify SSL certificates. Defaults to true.
|
||||
* @param int $timeout Optional. Timeout value to use in seconds. Defaults to 10.
|
||||
* @param int $retries Optional. Number of retry attempts to do if a status code was thrown that is worth
|
||||
* retrying. Defaults to 2.
|
||||
*/
|
||||
public function __construct($ssl_verify = \true, $timeout = self::DEFAULT_TIMEOUT, $retries = self::DEFAULT_RETRIES)
|
||||
{
|
||||
if (!\is_int($timeout) || $timeout < 0) {
|
||||
$timeout = self::DEFAULT_TIMEOUT;
|
||||
}
|
||||
if (!\is_int($retries) || $retries < 0) {
|
||||
$retries = self::DEFAULT_RETRIES;
|
||||
}
|
||||
$this->ssl_verify = $ssl_verify;
|
||||
$this->timeout = $timeout;
|
||||
$this->retries = $retries;
|
||||
}
|
||||
/**
|
||||
* Do a GET request to retrieve the contents of a remote URL.
|
||||
*
|
||||
* @param string $url URL to get.
|
||||
* @param array $headers Optional. Associative array of headers to send with the request. Defaults to empty array.
|
||||
* @return Response Response for the executed request.
|
||||
* @throws FailedToGetFromRemoteUrl If retrieving the contents from the URL failed.
|
||||
*/
|
||||
public function get($url, $headers = [])
|
||||
{
|
||||
$retries_left = $this->retries;
|
||||
do {
|
||||
$args = ['method' => 'GET', 'timeout' => $this->timeout, 'sslverify' => $this->ssl_verify, 'headers' => $headers];
|
||||
$response = \wp_remote_get($url, $args);
|
||||
if ($response instanceof WP_Error) {
|
||||
return new RemoteGetRequestResponse($response->get_error_message(), [], 500);
|
||||
}
|
||||
if (empty($response['response']['code'])) {
|
||||
return new RemoteGetRequestResponse(!empty($response['response']['message']) ? $response['response']['message'] : 'Unknown error', [], 500);
|
||||
}
|
||||
$status = $response['response']['code'];
|
||||
if ($status < 200 || $status >= 300) {
|
||||
if (!$retries_left || \in_array($status, self::RETRYABLE_STATUS_CODES, \true) === \false) {
|
||||
throw FailedToGetFromRemoteUrl::withHttpStatus($url, $status);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$headers = $response['headers'];
|
||||
if ($headers instanceof Traversable) {
|
||||
$headers = \iterator_to_array($headers);
|
||||
}
|
||||
if (!\is_array($headers)) {
|
||||
$headers = [];
|
||||
}
|
||||
return new RemoteGetRequestResponse($response['body'], $headers, (int) $status);
|
||||
} while ($retries_left--);
|
||||
// This should never be triggered, but we want to ensure we always have a typed return value,
|
||||
// to make PHPStan happy.
|
||||
return new RemoteGetRequestResponse('', [], 500);
|
||||
}
|
||||
}
|
196
wp-content/plugins/web-stories/third-party/vendor/ampproject/amp-wp/src/ValidationExemption.php
vendored
Normal file
196
wp-content/plugins/web-stories/third-party/vendor/ampproject/amp-wp/src/ValidationExemption.php
vendored
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class ValidationExemption.
|
||||
*
|
||||
* @package AmpProject\AmpWP
|
||||
*/
|
||||
namespace Google\Web_Stories_Dependencies\AmpProject\AmpWP;
|
||||
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Dom\Element;
|
||||
use DOMAttr;
|
||||
use DOMNode;
|
||||
/**
|
||||
* Helper functionality to deal with validation exemptions.
|
||||
*
|
||||
* @todo This could be made into a Service that contains a collection of nodes which were PX-verified or AMP-unvalidated. In this way, there would be no need to add attributes to DOM nodes, and non-element/attribute nodes could be marked, and it would be faster to see if there are any such exempted nodes.
|
||||
*
|
||||
* @package AmpProject\AmpWP
|
||||
* @since 2.2
|
||||
* @internal
|
||||
* @see \AmpProject\DevMode
|
||||
*/
|
||||
final class ValidationExemption
|
||||
{
|
||||
/**
|
||||
* HTML attribute to indicate one or more attributes have been verified for PX from AMP validation.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const PX_VERIFIED_ATTRS_ATTRIBUTE = 'data-px-verified-attrs';
|
||||
/**
|
||||
* HTML attribute to indicate an tag/element has been verified for PX.
|
||||
*
|
||||
* The difference here with `data-amp-unvalidated-tag` is that the PX-verified means that the tag will work
|
||||
* properly with CSS tree shaking.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const PX_VERIFIED_TAG_ATTRIBUTE = 'data-px-verified-tag';
|
||||
/**
|
||||
* HTML attribute to indicate one or more attributes are exempted from AMP validation.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AMP_UNVALIDATED_ATTRS_ATTRIBUTE = 'data-amp-unvalidated-attrs';
|
||||
/**
|
||||
* HTML attribute to indicate an tag/element is exempted from AMP validation.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const AMP_UNVALIDATED_TAG_ATTRIBUTE = 'data-amp-unvalidated-tag';
|
||||
/**
|
||||
* Check whether PX is verified for node.
|
||||
*
|
||||
* This means that it is exempted from AMP validation, but it doesn't mean the PX is negatively impacted.
|
||||
*
|
||||
* @param DOMNode $node Node.
|
||||
* @return bool Whether node marked as being PX-verified.
|
||||
*/
|
||||
public static function is_px_verified_for_node(DOMNode $node)
|
||||
{
|
||||
if ($node instanceof Element) {
|
||||
return $node->hasAttribute(self::PX_VERIFIED_TAG_ATTRIBUTE);
|
||||
} elseif ($node instanceof DOMAttr) {
|
||||
return self::check_for_attribute_token_list_membership($node, self::PX_VERIFIED_ATTRS_ATTRIBUTE);
|
||||
}
|
||||
return \false;
|
||||
}
|
||||
/**
|
||||
* Mark node as being PX-verified.
|
||||
*
|
||||
* @param DOMNode $node Node.
|
||||
* @return bool Whether successful.
|
||||
*/
|
||||
public static function mark_node_as_px_verified(DOMNode $node)
|
||||
{
|
||||
return self::mark_node_with_exemption_attribute($node, self::PX_VERIFIED_TAG_ATTRIBUTE, self::PX_VERIFIED_ATTRS_ATTRIBUTE);
|
||||
}
|
||||
/**
|
||||
* Mark node as not being AMP-validated.
|
||||
*
|
||||
* @param DOMNode $node Node.
|
||||
* @return bool Whether successful.
|
||||
*/
|
||||
public static function mark_node_as_amp_unvalidated(DOMNode $node)
|
||||
{
|
||||
return self::mark_node_with_exemption_attribute($node, self::AMP_UNVALIDATED_TAG_ATTRIBUTE, self::AMP_UNVALIDATED_ATTRS_ATTRIBUTE);
|
||||
}
|
||||
/**
|
||||
* Mark node with exemption attribute.
|
||||
*
|
||||
* @param DOMNode $node Node.
|
||||
* @param string $tag_attribute_name Tag attribute name.
|
||||
* @param string $attrs_attribute_name Attributes attribute name.
|
||||
* @return bool
|
||||
*/
|
||||
private static function mark_node_with_exemption_attribute(DOMNode $node, $tag_attribute_name, $attrs_attribute_name)
|
||||
{
|
||||
if ($node instanceof Element) {
|
||||
if (!$node->hasAttribute($tag_attribute_name)) {
|
||||
if (null === $node->ownerDocument) {
|
||||
return \false;
|
||||
// @codeCoverageIgnore
|
||||
}
|
||||
$node->setAttributeNode($node->ownerDocument->createAttribute($tag_attribute_name));
|
||||
}
|
||||
return \true;
|
||||
} elseif ($node instanceof DOMAttr) {
|
||||
$element = $node->parentNode;
|
||||
if (!$element instanceof Element) {
|
||||
return \false;
|
||||
// @codeCoverageIgnore
|
||||
}
|
||||
$attr_value = $element->getAttribute($attrs_attribute_name);
|
||||
if ($attr_value) {
|
||||
$attr_value .= ' ' . $node->nodeName;
|
||||
} else {
|
||||
$attr_value = $node->nodeName;
|
||||
}
|
||||
$element->setAttribute($attrs_attribute_name, $attr_value);
|
||||
return \true;
|
||||
}
|
||||
return \false;
|
||||
}
|
||||
/**
|
||||
* Check whether AMP is unvalidated for node.
|
||||
*
|
||||
* This means that it is exempted from AMP validation and it may negatively impact PX.
|
||||
*
|
||||
* @param DOMNode $node Node.
|
||||
* @return bool Whether node marked as being AMP-unvalidated.
|
||||
*/
|
||||
public static function is_amp_unvalidated_for_node(DOMNode $node)
|
||||
{
|
||||
if ($node instanceof Element) {
|
||||
return $node->hasAttribute(self::AMP_UNVALIDATED_TAG_ATTRIBUTE);
|
||||
} elseif ($node instanceof DOMAttr) {
|
||||
return self::check_for_attribute_token_list_membership($node, self::AMP_UNVALIDATED_ATTRS_ATTRIBUTE);
|
||||
}
|
||||
return \false;
|
||||
}
|
||||
/**
|
||||
* Check whether the given attribute node is mentioned among the token list of another supplied attribute.
|
||||
*
|
||||
* @param DOMAttr $attr Attribute node to check for.
|
||||
* @param string $token_list_attr_name Attribute name that has the token list value.
|
||||
*
|
||||
* @return bool Whether membership is present.
|
||||
*/
|
||||
private static function check_for_attribute_token_list_membership(DOMAttr $attr, $token_list_attr_name)
|
||||
{
|
||||
$element = $attr->parentNode;
|
||||
if (!$element instanceof Element) {
|
||||
return \false;
|
||||
// @codeCoverageIgnore
|
||||
}
|
||||
if (!$element->hasAttribute($token_list_attr_name)) {
|
||||
return \false;
|
||||
}
|
||||
return \in_array($attr->nodeName, \preg_split('/\\s+/', $element->getAttribute($token_list_attr_name)), \true);
|
||||
}
|
||||
/**
|
||||
* Check whether the provided document has nodes which are not AMP-validated.
|
||||
*
|
||||
* @param Document $document Document for which to check whether dev mode is active.
|
||||
* @return bool Whether the document is in dev mode.
|
||||
*/
|
||||
public static function is_document_with_amp_unvalidated_nodes(Document $document)
|
||||
{
|
||||
return self::is_document_containing_attributes($document, [self::AMP_UNVALIDATED_TAG_ATTRIBUTE, self::AMP_UNVALIDATED_ATTRS_ATTRIBUTE]);
|
||||
}
|
||||
/**
|
||||
* Check whether the provided document has nodes which are PX-verified.
|
||||
*
|
||||
* @param Document $document Document for which to check whether dev mode is active.
|
||||
* @return bool Whether the document is in dev mode.
|
||||
*/
|
||||
public static function is_document_with_px_verified_nodes(Document $document)
|
||||
{
|
||||
return self::is_document_containing_attributes($document, [self::PX_VERIFIED_TAG_ATTRIBUTE, self::PX_VERIFIED_ATTRS_ATTRIBUTE]);
|
||||
}
|
||||
/**
|
||||
* Check whether a document contains the given attribute names.
|
||||
*
|
||||
* @param Document $document Document.
|
||||
* @param string[] $attribute_names Attribute names.
|
||||
* @return bool Whether attributes exist in the document.
|
||||
*/
|
||||
private static function is_document_containing_attributes(Document $document, $attribute_names)
|
||||
{
|
||||
return $document->xpath->query(\sprintf('//*[%s]', \join(' or ', \array_map(static function ($attr_name) {
|
||||
return "@{$attr_name}";
|
||||
}, $attribute_names))))->length > 0;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user