Commit realizado el 12:13:52 08-04-2024
This commit is contained in:
+19
@@ -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;
|
||||
}
|
||||
}
|
||||
+15
File diff suppressed because one or more lines are too long
+20
File diff suppressed because one or more lines are too long
+143
File diff suppressed because one or more lines are too long
+134
File diff suppressed because one or more lines are too long
+844
@@ -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 [];
|
||||
}
|
||||
}
|
||||
+46
@@ -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, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+278
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+106
@@ -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' => []],
|
||||
];
|
||||
}
|
||||
+288
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2943
File diff suppressed because it is too large
Load Diff
+2250
File diff suppressed because it is too large
Load Diff
+93
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+138
@@ -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');
|
||||
}
|
||||
}
|
||||
wp-content/plugins/web-stories/third-party/vendor/ampproject/amp-wp/includes/uninstall-functions.php
Vendored
+142
@@ -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();
|
||||
}
|
||||
}
|
||||
+392
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+351
@@ -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();
|
||||
}
|
||||
}
|
||||
+28
@@ -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;
|
||||
}
|
||||
}
|
||||
+3065
File diff suppressed because it is too large
Load Diff
+2023
File diff suppressed because it is too large
Load Diff
+179
@@ -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;
|
||||
}
|
||||
}
|
||||
+115
@@ -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;
|
||||
}
|
||||
}
|
||||
+122
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+196
@@ -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