Commit realizado el 12:13:52 08-04-2024
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Canonical_Sanitizer.
|
||||
*
|
||||
* @link https://github.com/googleforcreators/web-stories-wp
|
||||
*
|
||||
* @copyright 2020 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\AMP;
|
||||
|
||||
use DOMElement;
|
||||
use DOMNode;
|
||||
use DOMNodeList;
|
||||
use Google\Web_Stories_Dependencies\AMP_Base_Sanitizer;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
|
||||
|
||||
/**
|
||||
* Canonical sanitizer class.
|
||||
*
|
||||
* Ensures link[rel=canonical] exists on the page,
|
||||
* as some plugins might have removed it in the meantime
|
||||
* or the user might be viewing a draft.
|
||||
*
|
||||
* Only needed when the AMP plugin is not active, as the plugin
|
||||
* handles that already.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @link https://github.com/googleforcreators/web-stories-wp/issues/4193
|
||||
* @link https://github.com/googleforcreators/web-stories-wp/pull/8169
|
||||
* @see \AMP_Theme_Support::ensure_required_markup()
|
||||
*/
|
||||
class Canonical_Sanitizer extends AMP_Base_Sanitizer {
|
||||
/**
|
||||
* Sanitize the HTML contained in the DOMDocument received by the constructor.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function sanitize(): void {
|
||||
$canonical_url = $this->args['canonical_url'];
|
||||
|
||||
$query = $this->dom->xpath->query( '//link[@rel="canonical"]', $this->dom->head );
|
||||
|
||||
// Remove any duplicate items first.
|
||||
if ( $query instanceof DOMNodeList && $query->length > 1 ) {
|
||||
for ( $i = 1; $i < $query->length; $i++ ) {
|
||||
$node = $query->item( $i );
|
||||
if ( $node ) {
|
||||
$this->dom->head->removeChild( $node );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DOMElement
|
||||
*
|
||||
* @var DOMElement|DOMNode $rel_canonical
|
||||
*/
|
||||
$rel_canonical = $query instanceof DOMNodeList ? $query->item( 0 ) : null;
|
||||
|
||||
if ( ! $rel_canonical instanceof DOMElement ) {
|
||||
$rel_canonical = $this->dom->createElement( Tag::LINK );
|
||||
|
||||
if ( $rel_canonical instanceof DOMElement ) {
|
||||
$rel_canonical->setAttribute( Attribute::REL, Attribute::REL_CANONICAL );
|
||||
$this->dom->head->appendChild( $rel_canonical );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $rel_canonical instanceof DOMElement ) {
|
||||
// Ensure link[rel=canonical] has a non-empty href attribute.
|
||||
if ( empty( $rel_canonical->getAttribute( Attribute::HREF ) ) ) {
|
||||
$rel_canonical->setAttribute( Attribute::HREF, (string) $canonical_url );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/**
|
||||
* Class AMP_Story_Sanitizer.
|
||||
*
|
||||
* @link https://github.com/googleforcreators/web-stories-wp
|
||||
*
|
||||
* @copyright 2020 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\AMP\Integration;
|
||||
|
||||
use AMP_Base_Sanitizer;
|
||||
use Google\Web_Stories\AMP\Traits\Sanitization_Utils;
|
||||
|
||||
/**
|
||||
* AMP Story sanitizer.
|
||||
*
|
||||
* Like Story_Sanitizer, but for use with the AMP WordPress plugin.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @see Story_Sanitizer
|
||||
*/
|
||||
class AMP_Story_Sanitizer extends AMP_Base_Sanitizer {
|
||||
use Sanitization_Utils;
|
||||
|
||||
/**
|
||||
* Sanitize the HTML contained in the DOMDocument received by the constructor.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function sanitize(): void {
|
||||
$this->transform_html_start_tag( $this->dom );
|
||||
$this->transform_a_tags( $this->dom );
|
||||
$this->use_semantic_heading_tags( $this->dom );
|
||||
$this->add_publisher_logo( $this->dom, $this->args['publisher_logo'] );
|
||||
$this->add_publisher( $this->dom, $this->args['publisher'] );
|
||||
$this->add_poster_images( $this->dom, $this->args['poster_images'] );
|
||||
// This needs to be called before use_semantic_heading_tags() because it relies on the style attribute.
|
||||
$this->deduplicate_inline_styles( $this->dom );
|
||||
$this->add_video_cache( $this->dom, $this->args['video_cache'] );
|
||||
$this->remove_blob_urls( $this->dom );
|
||||
$this->sanitize_srcset( $this->dom );
|
||||
$this->sanitize_amp_story_page_outlink( $this->dom );
|
||||
$this->remove_page_template_placeholder_images( $this->dom );
|
||||
$this->sanitize_title_and_meta_description( $this->dom, $this->args['title_tag'], $this->args['description'] );
|
||||
}
|
||||
}
|
124
wp-content/plugins/web-stories/includes/AMP/Meta_Sanitizer.php
Normal file
124
wp-content/plugins/web-stories/includes/AMP/Meta_Sanitizer.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Meta_Sanitizer.
|
||||
*
|
||||
* @link https://github.com/googleforcreators/web-stories-wp
|
||||
*
|
||||
* @copyright 2020 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\AMP;
|
||||
|
||||
use Google\Web_Stories_Dependencies\AMP_Meta_Sanitizer;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
|
||||
|
||||
/**
|
||||
* Meta sanitizer.
|
||||
*
|
||||
* Sanitizes meta tags found in the header.
|
||||
*
|
||||
* This version avoids using amp_get_boilerplate_stylesheets().
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @see amp_get_boilerplate_stylesheets()
|
||||
* @see AMP_Meta_Sanitizer
|
||||
*/
|
||||
class Meta_Sanitizer extends AMP_Meta_Sanitizer {
|
||||
/**
|
||||
* Always ensure we have a style[amp-boilerplate] and a noscript>style[amp-boilerplate].
|
||||
*
|
||||
* The AMP boilerplate ({@link https://amp.dev/documentation/guides-and-tutorials/stories/learn/spec/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."
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.NPathComplexity)
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @link https://amp.dev/documentation/guides-and-tutorials/optimize-and-measure/optimize_amp/#optimize-the-amp-runtime-loading
|
||||
*/
|
||||
protected function ensure_boilerplate_is_present(): void {
|
||||
$style = null;
|
||||
$styles = $this->dom->xpath->query( './style[ @amp-boilerplate ]', $this->dom->head );
|
||||
|
||||
if ( $styles ) {
|
||||
$style = $styles->item( 0 );
|
||||
}
|
||||
|
||||
if ( ! $style ) {
|
||||
$style = $this->dom->createElement( Tag::STYLE );
|
||||
if ( $style ) {
|
||||
$style->setAttribute( Attribute::AMP_BOILERPLATE, '' );
|
||||
$style->appendChild( $this->dom->createTextNode( $this->get_boilerplate_stylesheets()[0] ) );
|
||||
}
|
||||
} elseif ( $style->parentNode ) {
|
||||
$style->parentNode->removeChild( $style ); // So we can move it.
|
||||
}
|
||||
if ( $style ) {
|
||||
$this->dom->head->appendChild( $style );
|
||||
}
|
||||
|
||||
$noscript = null;
|
||||
$noscripts = $this->dom->xpath->query( './noscript[ style[ @amp-boilerplate ] ]', $this->dom->head );
|
||||
|
||||
if ( $noscripts ) {
|
||||
$noscript = $noscripts->item( 0 );
|
||||
}
|
||||
|
||||
if ( ! $noscript ) {
|
||||
$noscript = $this->dom->createElement( Tag::NOSCRIPT );
|
||||
$style = $this->dom->createElement( Tag::STYLE );
|
||||
if ( $style && $noscript ) {
|
||||
$style->setAttribute( Attribute::AMP_BOILERPLATE, '' );
|
||||
$style->appendChild( $this->dom->createTextNode( $this->get_boilerplate_stylesheets()[1] ) );
|
||||
$noscript->appendChild( $style );
|
||||
}
|
||||
} elseif ( $noscript->parentNode ) {
|
||||
$noscript->parentNode->removeChild( $noscript ); // So we can move it.
|
||||
}
|
||||
|
||||
if ( $noscript ) {
|
||||
$this->dom->head->appendChild( $noscript );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AMP boilerplate stylesheets.
|
||||
*
|
||||
* Clone of amp_get_boilerplate_stylesheets().
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @link https://www.ampproject.org/docs/reference/spec#boilerplate
|
||||
* @see amp_get_boilerplate_stylesheets()
|
||||
*
|
||||
* @return string[] Stylesheets, where first is contained in style[amp-boilerplate] and the second in noscript>style[amp-boilerplate].
|
||||
*/
|
||||
protected function get_boilerplate_stylesheets(): array {
|
||||
return [
|
||||
'body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}',
|
||||
'body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}',
|
||||
];
|
||||
}
|
||||
}
|
186
wp-content/plugins/web-stories/includes/AMP/Optimization.php
Normal file
186
wp-content/plugins/web-stories/includes/AMP/Optimization.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Optimization
|
||||
*
|
||||
* @link https://github.com/googleforcreators/web-stories-wp
|
||||
*
|
||||
* @copyright 2020 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\AMP;
|
||||
|
||||
use Google\Web_Stories_Dependencies\AmpProject\AmpWP\RemoteRequest\CachedRemoteGetRequest;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\AmpWP\RemoteRequest\WpHttpRemoteGetRequest;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\Configuration;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\Configuration\AmpStoryCssOptimizerConfiguration;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\DefaultConfiguration;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\Error;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\ErrorCollection;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\LocalFallback;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\TransformationEngine;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\Transformer\AmpRuntimeCss;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\Transformer\AmpStoryCssOptimizer;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\Transformer\MinifyHtml;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\Transformer\OptimizeAmpBind;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\Transformer\OptimizeHeroImages;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\Transformer\RewriteAmpUrls;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\Transformer\ServerSideRendering;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\Transformer\TransformedIdentifier;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\RemoteRequest\FallbackRemoteGetRequest;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\RemoteRequest\FilesystemRemoteGetRequest;
|
||||
|
||||
/**
|
||||
* Optimization class.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
class Optimization {
|
||||
/**
|
||||
* Optimizes a document.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @param Document $document Document instance.
|
||||
*/
|
||||
public function optimize_document( Document $document ): void {
|
||||
$errors = new ErrorCollection();
|
||||
$this->get_optimizer()->optimizeDom( $document, $errors );
|
||||
if ( \count( $errors ) > 0 ) {
|
||||
/**
|
||||
* Error list.
|
||||
*
|
||||
* @var Error[] $errors_array Error list.
|
||||
*/
|
||||
$errors_array = iterator_to_array( $errors );
|
||||
|
||||
$error_messages = array_filter(
|
||||
array_map(
|
||||
static function ( Error $error ) {
|
||||
// Hidden because amp-story is a render-delaying extension.
|
||||
if ( 'CannotRemoveBoilerplate' === $error->getCode() ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ' - ' . $error->getCode() . ': ' . $error->getMessage();
|
||||
},
|
||||
$errors_array
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! empty( $error_messages ) ) {
|
||||
$document->head->appendChild(
|
||||
$document->createComment( "\n" . __( 'AMP optimization could not be completed due to the following:', 'web-stories' ) . "\n" . implode( "\n", $error_messages ) . "\n" )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimizer instance to use.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @link https://github.com/ampproject/amp-wp/blob/8856284d90fc8558c30acc029becd352ae26e4e1/includes/class-amp-theme-support.php#L2235-L2255
|
||||
* @see AMP_Theme_Support::get_optimizer
|
||||
*
|
||||
* @return TransformationEngine Optimizer transformation engine to use.
|
||||
*/
|
||||
private function get_optimizer(): TransformationEngine {
|
||||
$configuration = self::get_optimizer_configuration();
|
||||
|
||||
$fallback_remote_request_pipeline = new FallbackRemoteGetRequest(
|
||||
new WpHttpRemoteGetRequest(),
|
||||
new FilesystemRemoteGetRequest( LocalFallback::getMappings() )
|
||||
);
|
||||
|
||||
$cached_remote_request = new CachedRemoteGetRequest( $fallback_remote_request_pipeline, WEEK_IN_SECONDS );
|
||||
|
||||
return new TransformationEngine(
|
||||
$configuration,
|
||||
$cached_remote_request
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AmpProject\Optimizer configuration object to use.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @link https://github.com/ampproject/amp-wp/blob/5405daa38e65f0ec16ffc920014d0110b03ee773/src/Optimizer/AmpWPConfiguration.php#L43-L78
|
||||
* @see AmpWPConfiguration::apply_filters()
|
||||
*
|
||||
* @return Configuration Optimizer configuration to use.
|
||||
*/
|
||||
private static function get_optimizer_configuration(): Configuration {
|
||||
$transformers = Configuration::DEFAULT_TRANSFORMERS;
|
||||
|
||||
$transformers[] = AmpStoryCssOptimizer::class;
|
||||
|
||||
/**
|
||||
* Filter whether the AMP Optimizer should use server-side rendering or not.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @param bool $enable_ssr Whether the AMP Optimizer should use server-side rendering or not.
|
||||
*/
|
||||
$enable_ssr = apply_filters( 'web_stories_enable_ssr', true );
|
||||
|
||||
// In debugging mode, we don't use server-side rendering, as it further obfuscates the HTML markup.
|
||||
if ( ! $enable_ssr ) {
|
||||
$transformers = array_diff(
|
||||
$transformers,
|
||||
[
|
||||
AmpRuntimeCss::class,
|
||||
OptimizeHeroImages::class,
|
||||
OptimizeAmpBind::class,
|
||||
RewriteAmpUrls::class,
|
||||
ServerSideRendering::class,
|
||||
TransformedIdentifier::class,
|
||||
AmpStoryCssOptimizer::class,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$configuration = [
|
||||
Configuration::KEY_TRANSFORMERS => $transformers,
|
||||
AmpStoryCssOptimizer::class => [
|
||||
AmpStoryCssOptimizerConfiguration::OPTIMIZE_AMP_STORY => true,
|
||||
],
|
||||
MinifyHtml::class => [
|
||||
// Prevents issues with rounding floats, relevant for things like shopping (product prices).
|
||||
Configuration\MinifyHtmlConfiguration::MINIFY_JSON => false,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Filter the configuration to be used for the AMP Optimizer.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @param array $configuration Associative array of configuration data.
|
||||
*/
|
||||
$configuration = apply_filters( 'web_stories_amp_optimizer_config', $configuration );
|
||||
|
||||
return new DefaultConfiguration( $configuration );
|
||||
}
|
||||
}
|
243
wp-content/plugins/web-stories/includes/AMP/Output_Buffer.php
Normal file
243
wp-content/plugins/web-stories/includes/AMP/Output_Buffer.php
Normal file
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Output_Buffer
|
||||
*
|
||||
* @link https://github.com/googleforcreators/web-stories-wp
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\AMP;
|
||||
|
||||
use Google\Web_Stories\Context;
|
||||
use Google\Web_Stories\Exception\SanitizationException;
|
||||
use Google\Web_Stories\Infrastructure\Conditional;
|
||||
use Google\Web_Stories\Service_Base;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Output buffer class.
|
||||
*
|
||||
* Largely copied from AMP_Theme_Support.
|
||||
*
|
||||
* @since 1.10.0
|
||||
*
|
||||
* @see \AMP_Theme_Support
|
||||
*/
|
||||
class Output_Buffer extends Service_Base implements Conditional {
|
||||
|
||||
/**
|
||||
* Whether output buffering has started.
|
||||
*/
|
||||
protected bool $is_output_buffering = false;
|
||||
|
||||
/**
|
||||
* Sanitization instance.
|
||||
*
|
||||
* @var Sanitization Sanitization instance.
|
||||
*/
|
||||
private Sanitization $sanitization;
|
||||
|
||||
/**
|
||||
* Optimization instance.
|
||||
*
|
||||
* @var Optimization Optimization instance.
|
||||
*/
|
||||
private Optimization $optimization;
|
||||
|
||||
/**
|
||||
* Context instance.
|
||||
*
|
||||
* @var Context Context instance.
|
||||
*/
|
||||
private Context $context;
|
||||
|
||||
/**
|
||||
* Output_Buffer constructor.
|
||||
*
|
||||
* @since 1.10.0
|
||||
*
|
||||
* @param Sanitization $sanitization Sanitization instance.
|
||||
* @param Optimization $optimization Optimization instance.
|
||||
* @param Context $context Context instance.
|
||||
*/
|
||||
public function __construct( Sanitization $sanitization, Optimization $optimization, Context $context ) {
|
||||
$this->sanitization = $sanitization;
|
||||
$this->optimization = $optimization;
|
||||
$this->context = $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on instantiation.
|
||||
*
|
||||
* @since 1.10.0
|
||||
*/
|
||||
public function register(): void {
|
||||
/*
|
||||
* Start output buffering at very low priority for sake of plugins and themes that use template_redirect
|
||||
* instead of template_include.
|
||||
*/
|
||||
$this->start_output_buffering();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the action to use for registering the service.
|
||||
*
|
||||
* @since 1.10.0
|
||||
*
|
||||
* @return string Registration action to use.
|
||||
*/
|
||||
public static function get_registration_action(): string {
|
||||
return 'template_redirect';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the action priority to use for registering the service.
|
||||
*
|
||||
* @since 1.10.0
|
||||
*
|
||||
* @return int Registration action priority to use.
|
||||
*/
|
||||
public static function get_registration_action_priority(): int {
|
||||
return PHP_INT_MIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the conditional object is currently needed.
|
||||
*
|
||||
* If the AMP plugin is installed and available in a version >= than ours,
|
||||
* all sanitization and optimization should be delegated to the AMP plugin.
|
||||
* But ONLY if AMP logic has not been disabled through any of its available filters.
|
||||
*
|
||||
* @since 1.10.0
|
||||
*
|
||||
* @return bool Whether the conditional object is needed.
|
||||
*/
|
||||
public static function is_needed(): bool {
|
||||
$current_post = get_post();
|
||||
|
||||
$has_old_amp_version = ! \defined( '\AMP__VERSION' ) || ( \defined( '\AMP__VERSION' ) && version_compare( \AMP__VERSION, WEBSTORIES_AMP_VERSION, '<' ) );
|
||||
$amp_available = \function_exists( 'amp_is_available' ) && amp_is_available();
|
||||
$amp_enabled = \function_exists( 'amp_is_enabled' ) && amp_is_enabled(); // Technically an internal method.
|
||||
$amp_initialized = did_action( 'amp_init' ) > 0;
|
||||
$amp_supported_post = \function_exists( 'amp_is_post_supported' ) && amp_is_post_supported( $current_post->ID ?? 0 );
|
||||
|
||||
return $has_old_amp_version || ! $amp_available || ! $amp_enabled || ! $amp_initialized || ! $amp_supported_post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start output buffering.
|
||||
*
|
||||
* @since 1.10.0
|
||||
*
|
||||
* @see Output_Buffer::finish_output_buffering()
|
||||
*/
|
||||
public function start_output_buffering(): void {
|
||||
if ( ! $this->context->is_web_story() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
ob_start( [ $this, 'finish_output_buffering' ] );
|
||||
$this->is_output_buffering = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether output buffering has started.
|
||||
*
|
||||
* @since 1.10.0
|
||||
*
|
||||
* @see Output_Buffer::start_output_buffering()
|
||||
* @see Output_Buffer::finish_output_buffering()
|
||||
*
|
||||
* @return bool Whether output buffering has started.
|
||||
*/
|
||||
public function is_output_buffering(): bool {
|
||||
return $this->is_output_buffering;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish output buffering.
|
||||
*
|
||||
* @since 1.10.0
|
||||
*
|
||||
* @see Output_Buffer::start_output_buffering()
|
||||
*
|
||||
* @param string $response Buffered Response.
|
||||
* @return string Processed Response.
|
||||
*/
|
||||
public function finish_output_buffering( string $response ): string {
|
||||
$this->is_output_buffering = false;
|
||||
|
||||
try {
|
||||
$response = $this->prepare_response( $response );
|
||||
} catch ( \Error $error ) { // Only PHP 7+.
|
||||
$response = $this->render_error_page( $error );
|
||||
} catch ( \Exception $exception ) {
|
||||
$response = $this->render_error_page( $exception );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process response to ensure AMP validity.
|
||||
*
|
||||
* @since 1.10.0
|
||||
*
|
||||
* @param string $response HTML document response. By default it expects a complete document.
|
||||
* @return string AMP document response.
|
||||
*/
|
||||
public function prepare_response( string $response ): string {
|
||||
// Enforce UTF-8 encoding as it is a requirement for AMP.
|
||||
if ( ! headers_sent() ) {
|
||||
header( 'Content-Type: text/html; charset=utf-8' );
|
||||
}
|
||||
|
||||
$dom = Document::fromHtml( $response );
|
||||
|
||||
if ( ! $dom instanceof Document ) {
|
||||
return $this->render_error_page( SanitizationException::from_document_parse_error() );
|
||||
}
|
||||
|
||||
$this->sanitization->sanitize_document( $dom );
|
||||
$this->optimization->optimize_document( $dom );
|
||||
|
||||
return $dom->saveHTML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render error page.
|
||||
*
|
||||
* @since 1.10.0
|
||||
*
|
||||
* @param Throwable $throwable Exception or (as of PHP7) Error.
|
||||
* @return string Error page.
|
||||
*/
|
||||
private function render_error_page( Throwable $throwable ): string {
|
||||
return esc_html__( 'There was an error generating the web story, probably because of a server misconfiguration. Try contacting your hosting provider or open a new support request.', 'web-stories' ) .
|
||||
"\n" .
|
||||
"\n" .
|
||||
// translators: 1: error message. 2: location.
|
||||
sprintf( esc_html__( 'Error message: %1$s (%2$s)', 'web-stories' ), $throwable->getMessage(), $throwable->getFile() . ':' . $throwable->getLine() );
|
||||
}
|
||||
}
|
547
wp-content/plugins/web-stories/includes/AMP/Sanitization.php
Normal file
547
wp-content/plugins/web-stories/includes/AMP/Sanitization.php
Normal file
@@ -0,0 +1,547 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Sanitization
|
||||
*
|
||||
* @link https://github.com/googleforcreators/web-stories-wp
|
||||
*
|
||||
* @copyright 2020 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\AMP;
|
||||
|
||||
use DOMElement;
|
||||
use DOMNode;
|
||||
use Google\Web_Stories\Model\Story;
|
||||
use Google\Web_Stories\Settings;
|
||||
use Google\Web_Stories\Story_Post_Type;
|
||||
use Google\Web_Stories_Dependencies\AMP_Allowed_Tags_Generated;
|
||||
use Google\Web_Stories_Dependencies\AMP_Content_Sanitizer;
|
||||
use Google\Web_Stories_Dependencies\AMP_Dev_Mode_Sanitizer;
|
||||
use Google\Web_Stories_Dependencies\AMP_DOM_Utils;
|
||||
use Google\Web_Stories_Dependencies\AMP_Layout_Sanitizer;
|
||||
use Google\Web_Stories_Dependencies\AMP_Script_Sanitizer;
|
||||
use Google\Web_Stories_Dependencies\AMP_Style_Sanitizer;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Amp;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Extension;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Html\Tag;
|
||||
|
||||
/**
|
||||
* Sanitization class.
|
||||
*
|
||||
* Largely copied from AMP_Theme_Support.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @see \AMP_Theme_Support
|
||||
*/
|
||||
class Sanitization {
|
||||
/**
|
||||
* Settings instance.
|
||||
*
|
||||
* @var Settings Settings instance.
|
||||
*/
|
||||
private Settings $settings;
|
||||
|
||||
/**
|
||||
* Analytics constructor.
|
||||
*
|
||||
* @since 1.12.0
|
||||
*
|
||||
* @param Settings $settings Settings instance.
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( Settings $settings ) {
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a document.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @param Document $document Document instance.
|
||||
*/
|
||||
public function sanitize_document( Document $document ): void {
|
||||
$sanitizers = $this->get_sanitizers();
|
||||
|
||||
$result = AMP_Content_Sanitizer::sanitize_document( $document, $sanitizers, [] );
|
||||
|
||||
$this->ensure_required_markup( $document, $result['scripts'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation error callback.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @see AMP_Validation_Error_Taxonomy::get_validation_error_sanitization
|
||||
*
|
||||
* @param array{code: string} $error Error info, especially code.
|
||||
* @param array{node?: DOMElement|DOMNode} $data Additional data, including the node.
|
||||
* @return bool Whether the validation error should be sanitized.
|
||||
*/
|
||||
public function validation_error_callback( array $error, array $data = [] ): bool { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
||||
/**
|
||||
* Filters whether the validation error should be sanitized.
|
||||
*
|
||||
* Returning true this indicates that the validation error is acceptable
|
||||
* and should not be considered a blocker to render AMP. Returning null
|
||||
* means that the default status should be used.
|
||||
*
|
||||
* Note that the $node is not passed here to ensure that the filter can be
|
||||
* applied on validation errors that have been stored. Likewise, the $sources
|
||||
* are also omitted because these are only available during an explicit
|
||||
* validation request and so they are not suitable for plugins to vary
|
||||
* sanitization by.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @see AMP_Validation_Manager::is_sanitization_auto_accepted() Which controls whether an error is initially accepted or rejected for sanitization.
|
||||
*
|
||||
* @param bool $sanitized Whether the validation error should be sanitized.
|
||||
* @param array $error Validation error being sanitized.
|
||||
*/
|
||||
return apply_filters( 'web_stories_amp_validation_error_sanitized', true, $error );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds missing scripts.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD)
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @link https://github.com/ampproject/amp-wp/blob/2.1.3/includes/class-amp-theme-support.php#L1381-L1594
|
||||
* @see \AMP_Theme_Support::ensure_required_markup
|
||||
*
|
||||
* @param Document $document Document instance.
|
||||
* @param array<string,string> $scripts List of found scripts.
|
||||
*/
|
||||
protected function ensure_required_markup( Document $document, array $scripts ): void { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
|
||||
/**
|
||||
* Link elements.
|
||||
*
|
||||
* @var array{preconnect: \DOMElement[]|null,dns-prefetch: \DOMElement[]|null,preload: \DOMElement[]|null, prerender: \DOMElement[]|null, prefetch: \DOMElement[]|null }
|
||||
*/
|
||||
$links = [
|
||||
Attribute::REL_PRECONNECT => [
|
||||
// Include preconnect link for AMP CDN for browsers that don't support preload.
|
||||
AMP_DOM_Utils::create_node(
|
||||
$document,
|
||||
Tag::LINK,
|
||||
[
|
||||
Attribute::REL => Attribute::REL_PRECONNECT,
|
||||
Attribute::HREF => 'https://cdn.ampproject.org',
|
||||
]
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
// Obtain the existing AMP scripts.
|
||||
$amp_scripts = [];
|
||||
$ordered_scripts = [];
|
||||
$head_scripts = [];
|
||||
$runtime_src = 'https://cdn.ampproject.org/v0.js';
|
||||
|
||||
/**
|
||||
* Script element.
|
||||
*
|
||||
* @var DOMElement $script
|
||||
*/
|
||||
foreach ( $document->head->getElementsByTagName( Tag::SCRIPT ) as $script ) {
|
||||
$head_scripts[] = $script;
|
||||
}
|
||||
|
||||
foreach ( $head_scripts as $script ) {
|
||||
$src = $script->getAttribute( Attribute::SRC );
|
||||
|
||||
if ( ! $src || ! str_starts_with( $src, 'https://cdn.ampproject.org/' ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 'v0.js' === substr( $src, - \strlen( 'v0.js' ) ) ) {
|
||||
$amp_scripts[ Amp::RUNTIME ] = $script;
|
||||
} elseif ( $script->hasAttribute( Attribute::CUSTOM_ELEMENT ) ) {
|
||||
$amp_scripts[ $script->getAttribute( Attribute::CUSTOM_ELEMENT ) ] = $script;
|
||||
} elseif ( $script->hasAttribute( Attribute::CUSTOM_TEMPLATE ) ) {
|
||||
$amp_scripts[ $script->getAttribute( Attribute::CUSTOM_TEMPLATE ) ] = $script;
|
||||
}
|
||||
|
||||
// It will be added back further down.
|
||||
$document->head->removeChild( $script );
|
||||
}
|
||||
|
||||
$specs = $this->get_extension_sources();
|
||||
|
||||
// Create scripts for any components discovered from output buffering that are missing.
|
||||
foreach ( array_diff( array_keys( $scripts ), array_keys( $amp_scripts ) ) as $missing_script_handle ) {
|
||||
$attrs = [
|
||||
Attribute::SRC => $specs[ $missing_script_handle ],
|
||||
Attribute::ASYNC => '',
|
||||
];
|
||||
if ( Extension::MUSTACHE === $missing_script_handle ) {
|
||||
$attrs[ Attribute::CUSTOM_TEMPLATE ] = (string) $missing_script_handle;
|
||||
} else {
|
||||
$attrs[ Attribute::CUSTOM_ELEMENT ] = (string) $missing_script_handle;
|
||||
}
|
||||
|
||||
$amp_scripts[ $missing_script_handle ] = AMP_DOM_Utils::create_node( $document, Tag::SCRIPT, $attrs );
|
||||
}
|
||||
|
||||
// Remove scripts that had already been added but couldn't be detected from output buffering.
|
||||
$extension_specs = AMP_Allowed_Tags_Generated::get_extension_specs();
|
||||
$superfluous_script_handles = array_diff(
|
||||
array_keys( $amp_scripts ),
|
||||
[ ...array_keys( $scripts ), Amp::RUNTIME ]
|
||||
);
|
||||
|
||||
foreach ( $superfluous_script_handles as $superfluous_script_handle ) {
|
||||
if ( ! empty( $extension_specs[ $superfluous_script_handle ]['requires_usage'] ) ) {
|
||||
unset( $amp_scripts[ $superfluous_script_handle ] );
|
||||
}
|
||||
}
|
||||
|
||||
/* phpcs:ignore Squiz.PHP.CommentedOutCode.Found
|
||||
*
|
||||
* "2. Next, preload the AMP runtime v0.js <script> tag with <link as=script href=https://cdn.ampproject.org/v0.js rel=preload>.
|
||||
* The AMP runtime should start downloading as soon as possible because the AMP boilerplate hides the document via body { visibility:hidden }
|
||||
* until the AMP runtime has loaded. Preloading the AMP runtime tells the browser to download the script with a higher priority."
|
||||
* {@link https://amp.dev/documentation/guides-and-tutorials/optimize-and-measure/optimize_amp/ Optimize the AMP Runtime loading}
|
||||
*/
|
||||
$prioritized_preloads = [];
|
||||
if ( ! isset( $links[ Attribute::REL_PRELOAD ] ) ) {
|
||||
$links[ Attribute::REL_PRELOAD ] = [];
|
||||
}
|
||||
|
||||
$prioritized_preloads[] = AMP_DOM_Utils::create_node(
|
||||
$document,
|
||||
Tag::LINK,
|
||||
[
|
||||
Attribute::REL => Attribute::REL_PRELOAD,
|
||||
'as' => Tag::SCRIPT,
|
||||
Attribute::HREF => $runtime_src,
|
||||
]
|
||||
);
|
||||
|
||||
/*
|
||||
* "3. If your page includes render-delaying extensions (e.g., amp-experiment, amp-dynamic-css-classes, amp-story),
|
||||
* preload those extensions as they're required by the AMP runtime for rendering the page."
|
||||
*/
|
||||
$amp_script_handles = array_keys( $amp_scripts );
|
||||
foreach ( array_intersect( Amp::RENDER_DELAYING_EXTENSIONS, $amp_script_handles ) as $script_handle ) {
|
||||
if ( ! \in_array( $script_handle, Amp::RENDER_DELAYING_EXTENSIONS, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* AMP script element.
|
||||
*
|
||||
* @var DOMElement $script_element
|
||||
*/
|
||||
$script_element = $amp_scripts[ $script_handle ];
|
||||
|
||||
$prioritized_preloads[] = AMP_DOM_Utils::create_node(
|
||||
$document,
|
||||
Tag::LINK,
|
||||
[
|
||||
Attribute::REL => Attribute::REL_PRELOAD,
|
||||
'as' => Tag::SCRIPT,
|
||||
Attribute::HREF => $script_element->getAttribute( Attribute::SRC ),
|
||||
]
|
||||
);
|
||||
}
|
||||
$links[ Attribute::REL_PRELOAD ] = array_merge( $prioritized_preloads, $links[ Attribute::REL_PRELOAD ] );
|
||||
|
||||
// Store the last meta tag as the previous node to append to.
|
||||
$meta_tags = $document->head->getElementsByTagName( Tag::META );
|
||||
$previous_node = $meta_tags->length > 0 ? $meta_tags->item( $meta_tags->length - 1 ) : $document->head->firstChild;
|
||||
|
||||
/*
|
||||
* "4. Use preconnect to speedup the connection to other origin where the full resource URL is not known ahead of time,
|
||||
* for example, when using Google Fonts."
|
||||
*
|
||||
* Note that \AMP_Style_Sanitizer::process_link_element() will ensure preconnect links for Google Fonts are present.
|
||||
*/
|
||||
$link_relations = [ Attribute::REL_PRECONNECT, Attribute::REL_DNS_PREFETCH, Attribute::REL_PRELOAD, Attribute::REL_PRERENDER, Attribute::REL_PREFETCH ];
|
||||
foreach ( $link_relations as $rel ) {
|
||||
if ( ! isset( $links[ $rel ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link node.
|
||||
*
|
||||
* @var DOMElement $link
|
||||
*/
|
||||
foreach ( $links[ $rel ] as $link ) {
|
||||
if ( $link->parentNode ) {
|
||||
$link->parentNode->removeChild( $link ); // So we can move it.
|
||||
}
|
||||
if ( $previous_node && $previous_node->nextSibling ) {
|
||||
$document->head->insertBefore( $link, $previous_node->nextSibling );
|
||||
$previous_node = $link;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// "5. Load the AMP runtime."
|
||||
if ( isset( $amp_scripts[ Amp::RUNTIME ] ) ) {
|
||||
$ordered_scripts[ Amp::RUNTIME ] = $amp_scripts[ Amp::RUNTIME ];
|
||||
unset( $amp_scripts[ Amp::RUNTIME ] );
|
||||
} else {
|
||||
$script = $document->createElement( Tag::SCRIPT );
|
||||
if ( $script ) {
|
||||
$script->setAttribute( Attribute::ASYNC, '' );
|
||||
$script->setAttribute( Attribute::SRC, $runtime_src );
|
||||
$ordered_scripts[ Amp::RUNTIME ] = $script;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* "6. Specify the <script> tags for render-delaying extensions (e.g., amp-experiment amp-dynamic-css-classes and amp-story"
|
||||
*
|
||||
* {@link https://amp.dev/documentation/guides-and-tutorials/optimize-and-measure/optimize_amp/ AMP Hosting Guide}
|
||||
*/
|
||||
foreach ( Amp::RENDER_DELAYING_EXTENSIONS as $extension ) {
|
||||
if ( isset( $amp_scripts[ $extension ] ) ) {
|
||||
$ordered_scripts[ $extension ] = $amp_scripts[ $extension ];
|
||||
unset( $amp_scripts[ $extension ] );
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* "7. Specify the <script> tags for remaining extensions (e.g., amp-bind ...). These extensions are not render-delaying
|
||||
* and therefore should not be preloaded as they might take away important bandwidth for the initial render."
|
||||
*/
|
||||
ksort( $amp_scripts );
|
||||
$ordered_scripts = array_merge( $ordered_scripts, $amp_scripts );
|
||||
|
||||
/**
|
||||
* Script element.
|
||||
*
|
||||
* @var DOMElement $ordered_script
|
||||
*/
|
||||
foreach ( $ordered_scripts as $ordered_script ) {
|
||||
if ( $previous_node && $previous_node->nextSibling ) {
|
||||
$document->head->insertBefore( $ordered_script, $previous_node->nextSibling );
|
||||
$previous_node = $ordered_script;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns AMP extension URLs, keyed by extension name.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @link https://github.com/ampproject/amp-wp/blob/2.1.3/includes/amp-helper-functions.php#L876-L941
|
||||
* @see amp_register_default_scripts
|
||||
*
|
||||
* @return array<string,string> List of extensions and their URLs.
|
||||
*/
|
||||
protected function get_extension_sources(): array {
|
||||
$specs = [];
|
||||
// Register all AMP components as defined in the spec.
|
||||
foreach ( AMP_Allowed_Tags_Generated::get_extension_specs() as $extension_name => $extension_spec ) {
|
||||
$src = sprintf(
|
||||
'https://cdn.ampproject.org/v0/%s-%s.js',
|
||||
$extension_name,
|
||||
$extension_spec['latest']
|
||||
);
|
||||
|
||||
$specs[ $extension_name ] = $src;
|
||||
}
|
||||
|
||||
return $specs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether AMP dev mode is enabled.
|
||||
*
|
||||
* When enabled, the <html> element will get the data-ampdevmode attribute and the plugin will add the same attribute
|
||||
* to elements associated with the admin bar and other elements that are provided by the `amp_dev_mode_element_xpaths`
|
||||
* filter.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @link https://github.com/ampproject/amp-wp/blob/2.1.3/includes/amp-helper-functions.php#L1296-L1330
|
||||
* @see amp_is_dev_mode
|
||||
*
|
||||
* @return bool Whether AMP dev mode is enabled.
|
||||
*/
|
||||
protected function is_amp_dev_mode(): bool {
|
||||
// For the few sites that forcibly show the admin bar even when the user is logged out, only enable dev
|
||||
// mode if the user is actually logged in. This prevents the dev mode from being served to crawlers
|
||||
// when they index the AMP version.
|
||||
$dev_mode_enabled = ( is_admin_bar_showing() && is_user_logged_in() );
|
||||
|
||||
/**
|
||||
* Filters whether AMP dev mode is enabled.
|
||||
*
|
||||
* When enabled, the data-ampdevmode attribute will be added to the document element and it will allow the
|
||||
* attributes to be added to the admin bar. It will also add the attribute to all elements which match the
|
||||
* queries for the expressions returned by the 'web_stories_amp_dev_mode_element_xpaths' filter.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @param bool $dev_mode_enabled Whether AMP dev mode is enabled.
|
||||
*/
|
||||
return apply_filters( 'web_stories_amp_dev_mode_enabled', $dev_mode_enabled );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of sanitizers to use.
|
||||
*
|
||||
* This is replica of amp_get_content_sanitizers() to avoid
|
||||
* loading amp-helper-functions.php due to side-effects like
|
||||
* accessing options from the database, requiring AMP__VERSION,
|
||||
* and causing conflicts with our own amp_is_request() compat shim.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @link https://github.com/ampproject/amp-wp/blob/2.1.3/includes/amp-helper-functions.php#L1332-L1521
|
||||
* @link https://github.com/ampproject/amp-wp/blob/2.1.3/includes/validation/class-amp-validation-manager.php#L1691-L1730
|
||||
* @see amp_get_content_sanitizers
|
||||
* @see AMP_Validation_Manager::filter_sanitizer_args
|
||||
*
|
||||
* @return array<string,array<string,bool|string[]|string>> Sanitizers.
|
||||
*/
|
||||
protected function get_sanitizers(): array {
|
||||
// This fallback to get_permalink() ensures that there's a canonical link
|
||||
// even when previewing drafts.
|
||||
$canonical_url = wp_get_canonical_url();
|
||||
if ( ! $canonical_url ) {
|
||||
$canonical_url = get_permalink();
|
||||
}
|
||||
|
||||
$sanitizers = [
|
||||
AMP_Script_Sanitizer::class => [],
|
||||
AMP_Style_Sanitizer::class => [
|
||||
|
||||
/*
|
||||
* @todo Enable by default and allow filtering once AMP_Style_Sanitizer does not call AMP_Options_Manager
|
||||
* which in turn requires AMP__VERSION to be defined.
|
||||
*/
|
||||
'allow_transient_caching' => false,
|
||||
'use_document_element' => true,
|
||||
'dynamic_element_selectors' => [
|
||||
'amp-story-captions',
|
||||
],
|
||||
],
|
||||
Meta_Sanitizer::class => [],
|
||||
AMP_Layout_Sanitizer::class => [],
|
||||
Canonical_Sanitizer::class => [
|
||||
'canonical_url' => $canonical_url,
|
||||
],
|
||||
Tag_And_Attribute_Sanitizer::class => [],
|
||||
];
|
||||
|
||||
$post = get_queried_object();
|
||||
|
||||
if ( $post instanceof \WP_Post && Story_Post_Type::POST_TYPE_SLUG === get_post_type( $post ) ) {
|
||||
$video_cache_enabled = (bool) $this->settings->get_setting( $this->settings::SETTING_NAME_VIDEO_CACHE );
|
||||
|
||||
$story = new Story();
|
||||
$story->load_from_post( $post );
|
||||
|
||||
$poster_images = [
|
||||
'poster-portrait-src' => esc_url_raw( $story->get_poster_portrait() ),
|
||||
];
|
||||
|
||||
$sanitizers[ Story_Sanitizer::class ] = [
|
||||
'publisher_logo' => (string) $story->get_publisher_logo_url(),
|
||||
'publisher' => $story->get_publisher_name(),
|
||||
'poster_images' => array_filter( $poster_images ),
|
||||
'video_cache' => $video_cache_enabled,
|
||||
'title_tag' => wp_get_document_title(),
|
||||
'description' => wp_strip_all_tags( get_the_excerpt() ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the content sanitizers.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @param array $sanitizers Sanitizers.
|
||||
*/
|
||||
$sanitizers = apply_filters( 'web_stories_amp_sanitizers', $sanitizers );
|
||||
|
||||
if ( $this->is_amp_dev_mode() ) {
|
||||
/**
|
||||
* Filters the XPath queries for elements that should be enabled for dev mode.
|
||||
*
|
||||
* By supplying XPath queries to this filter, the data-ampdevmode attribute will automatically be added to the
|
||||
* root HTML element as well as to any elements that match the expressions. The attribute is added to the
|
||||
* elements prior to running any of the sanitizers.
|
||||
*
|
||||
* @since 1.3
|
||||
*
|
||||
* @param string[] $element_xpaths XPath element queries. Context is the root element.
|
||||
*/
|
||||
$dev_mode_xpaths = apply_filters( 'web_stories_amp_dev_mode_element_xpaths', [] );
|
||||
|
||||
if ( is_admin_bar_showing() ) {
|
||||
$dev_mode_xpaths[] = '//*[ @id = "wpadminbar" ]';
|
||||
$dev_mode_xpaths[] = '//*[ @id = "wpadminbar" ]//*';
|
||||
$dev_mode_xpaths[] = '//style[ @id = "admin-bar-inline-css" ]';
|
||||
}
|
||||
|
||||
$sanitizers = array_merge(
|
||||
[
|
||||
AMP_Dev_Mode_Sanitizer::class => [
|
||||
'element_xpaths' => $dev_mode_xpaths,
|
||||
],
|
||||
],
|
||||
$sanitizers
|
||||
);
|
||||
}
|
||||
|
||||
// Force certain sanitizers to be at end.
|
||||
// AMP_Style_Sanitizer needs to catch any CSS changes from previous sanitizers.
|
||||
// Tag_And_Attribute_Sanitizer must come at the end to clean up any remaining issues the other sanitizers didn't catch.
|
||||
foreach ( [
|
||||
AMP_Layout_Sanitizer::class,
|
||||
AMP_Style_Sanitizer::class,
|
||||
Meta_Sanitizer::class,
|
||||
Tag_And_Attribute_Sanitizer::class,
|
||||
] as $class_name ) {
|
||||
if ( isset( $sanitizers[ $class_name ] ) ) {
|
||||
$sanitizer = $sanitizers[ $class_name ];
|
||||
unset( $sanitizers[ $class_name ] );
|
||||
$sanitizers[ $class_name ] = $sanitizer;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $sanitizers as &$sanitizer ) {
|
||||
$sanitizer['validation_error_callback'] = [ $this, 'validation_error_callback' ];
|
||||
}
|
||||
|
||||
unset( $sanitizer );
|
||||
|
||||
return $sanitizers;
|
||||
}
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Story_Sanitizer.
|
||||
*
|
||||
* @link https://github.com/googleforcreators/web-stories-wp
|
||||
*
|
||||
* @copyright 2020 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\AMP;
|
||||
|
||||
use Google\Web_Stories\AMP\Traits\Sanitization_Utils;
|
||||
use Google\Web_Stories_Dependencies\AMP_Base_Sanitizer;
|
||||
|
||||
/**
|
||||
* Story sanitizer.
|
||||
*
|
||||
* Sanitizer for Web Stories related features.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
class Story_Sanitizer extends AMP_Base_Sanitizer {
|
||||
use Sanitization_Utils;
|
||||
|
||||
/**
|
||||
* Sanitize the HTML contained in the DOMDocument received by the constructor.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function sanitize(): void {
|
||||
$this->transform_html_start_tag( $this->dom );
|
||||
$this->transform_a_tags( $this->dom );
|
||||
$this->use_semantic_heading_tags( $this->dom );
|
||||
$this->add_publisher_logo( $this->dom, $this->args['publisher_logo'] );
|
||||
$this->add_publisher( $this->dom, $this->args['publisher'] );
|
||||
$this->add_poster_images( $this->dom, $this->args['poster_images'] );
|
||||
// This needs to be called before use_semantic_heading_tags() because it relies on the style attribute.
|
||||
$this->deduplicate_inline_styles( $this->dom );
|
||||
$this->add_video_cache( $this->dom, $this->args['video_cache'] );
|
||||
$this->remove_blob_urls( $this->dom );
|
||||
$this->sanitize_srcset( $this->dom );
|
||||
$this->sanitize_amp_story_page_outlink( $this->dom );
|
||||
$this->remove_page_template_placeholder_images( $this->dom );
|
||||
$this->sanitize_title_and_meta_description( $this->dom, $this->args['title_tag'], $this->args['description'] );
|
||||
}
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\AMP;
|
||||
|
||||
use Google\Web_Stories_Dependencies\AMP_Tag_And_Attribute_Sanitizer;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
|
||||
|
||||
/**
|
||||
* Strips the tags and attributes from the content that are not allowed by the AMP spec.
|
||||
*
|
||||
* @since 1.28.0
|
||||
*
|
||||
* @see AMP_Tag_And_Attribute_Sanitizer
|
||||
*/
|
||||
class Tag_And_Attribute_Sanitizer extends AMP_Tag_And_Attribute_Sanitizer {
|
||||
/**
|
||||
* AMP_Tag_And_Attribute_Sanitizer constructor.
|
||||
*
|
||||
* @since 1.28.0
|
||||
*
|
||||
* @param Document $dom DOM.
|
||||
* @param array<string, mixed> $args args.
|
||||
*/
|
||||
public function __construct( Document $dom, array $args = [] ) { // phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod.Found
|
||||
parent::__construct( $dom, $args );
|
||||
}
|
||||
}
|
@@ -0,0 +1,718 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait Sanitization_Utils.
|
||||
*
|
||||
* @link https://github.com/googleforcreators/web-stories-wp
|
||||
*
|
||||
* @copyright 2020 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2020 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\AMP\Traits;
|
||||
|
||||
use AmpProject\Dom\Document as AMP_Document;
|
||||
use DOMAttr;
|
||||
use DOMElement;
|
||||
use DOMNode;
|
||||
use DOMNodeList;
|
||||
use Google\Web_Stories_Dependencies\AmpProject\Dom\Document;
|
||||
|
||||
/**
|
||||
* Trait Sanitization_Utils
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
trait Sanitization_Utils {
|
||||
/**
|
||||
* Replaces the HTML start tag to make the language attributes dynamic.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @param Document|AMP_Document $document Document instance.
|
||||
*/
|
||||
private function transform_html_start_tag( $document ): void {
|
||||
$document->html->setAttribute( 'amp', '' );
|
||||
|
||||
// See get_language_attributes().
|
||||
if ( is_rtl() ) {
|
||||
$document->html->setAttribute( 'dir', 'rtl' );
|
||||
}
|
||||
|
||||
$lang = get_bloginfo( 'language' );
|
||||
if ( $lang ) {
|
||||
$document->html->setAttribute( 'lang', $lang );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform all hyperlinks to ensure they're always valid.
|
||||
*
|
||||
* Adds target="_blank" and rel="noreferrer" attributes.
|
||||
* Does not add rel="noreferrer" for same-origin links.
|
||||
*
|
||||
* Removes empty data-tooltip-icon and data-tooltip-text attributes
|
||||
* to prevent validation issues.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.NPathComplexity)
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @param Document|AMP_Document $document Document instance.
|
||||
*/
|
||||
private function transform_a_tags( $document ): void {
|
||||
$links = $document->getElementsByTagName( 'a' );
|
||||
|
||||
/**
|
||||
* The <a> element
|
||||
*
|
||||
* @var DOMElement $link The <a> element
|
||||
*/
|
||||
foreach ( $links as $link ) {
|
||||
$url = $link->getAttribute( 'href' );
|
||||
|
||||
$is_relative_link = str_starts_with( $url, '#' );
|
||||
|
||||
if ( ! $is_relative_link && ! $link->getAttribute( 'target' ) ) {
|
||||
$link->setAttribute( 'target', '_blank' );
|
||||
}
|
||||
|
||||
$is_link_to_same_origin = str_starts_with( $url, home_url() ) || $is_relative_link;
|
||||
|
||||
$rel = $link->getAttribute( 'rel' );
|
||||
|
||||
// Links to the same site should not have "noreferrer".
|
||||
// Other rel values should not be modified.
|
||||
// See https://github.com/googleforcreators/web-stories-wp/issues/9494.
|
||||
$rel = str_replace( 'noreferrer', '', $rel );
|
||||
if ( ! $is_link_to_same_origin ) {
|
||||
$rel .= ' noreferrer';
|
||||
}
|
||||
|
||||
if ( empty( $rel ) ) {
|
||||
$link->removeAttribute( 'rel' );
|
||||
} else {
|
||||
$link->setAttribute( 'rel', trim( $rel ) );
|
||||
}
|
||||
|
||||
if ( ! $link->getAttribute( 'data-tooltip-icon' ) ) {
|
||||
$link->removeAttribute( 'data-tooltip-icon' );
|
||||
}
|
||||
|
||||
if ( ! $link->getAttribute( 'data-tooltip-text' ) ) {
|
||||
$link->removeAttribute( 'data-tooltip-text' );
|
||||
}
|
||||
|
||||
// Extra hardening to catch links without a proper protocol.
|
||||
// Matches withProtocol() util in the editor.
|
||||
if (
|
||||
! $is_relative_link &&
|
||||
! str_starts_with( $url, 'http://' ) &&
|
||||
! str_starts_with( $url, 'https://' ) &&
|
||||
! str_starts_with( $url, 'tel:' ) &&
|
||||
! str_starts_with( $url, 'mailto:' )
|
||||
) {
|
||||
$link->setAttribute( 'href', 'https://' . $url );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms all paragraphs in a story to use semantic heading tags if needed.
|
||||
*
|
||||
* This logic here mirrors the getTextElementTagNames() function in the editor
|
||||
* in order to change simple <p> tags into <h1>, <h2> or <h3>, depending on font size.
|
||||
*
|
||||
* It is only relevant for older stories that haven't been updated in a while,
|
||||
* so we bail early if we find any existing headings in the story.
|
||||
*
|
||||
* Caveat: if a user forces *all* text elements to be paragraphs,
|
||||
* this sanitizer would still run and thus turn some paragraphs into headings.
|
||||
* This seems rather unlikely though.
|
||||
*
|
||||
* @since 1.18.0
|
||||
*
|
||||
* @link https://github.com/GoogleForCreators/web-stories-wp/issues/12850
|
||||
*
|
||||
* @param Document|AMP_Document $document Document instance.
|
||||
*/
|
||||
private function use_semantic_heading_tags( $document ): void {
|
||||
$h1 = $document->getElementsByTagName( 'h1' );
|
||||
$h2 = $document->getElementsByTagName( 'h2' );
|
||||
$h3 = $document->getElementsByTagName( 'h3' );
|
||||
|
||||
// When a story already contains any headings, we don't need to do anything further.
|
||||
if ( $h1->count() || $h2->count() || $h3->count() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pages = $document->getElementsByTagName( 'amp-story-page' );
|
||||
|
||||
/**
|
||||
* The <amp-story-page> element
|
||||
*
|
||||
* @var DOMElement $page The <amp-story-page> element
|
||||
*/
|
||||
foreach ( $pages as $page ) {
|
||||
$text_elements = $document->xpath->query( './/p[ contains( @class, "text-wrapper" ) ]', $page );
|
||||
|
||||
if ( ! $text_elements ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->use_semantic_heading_tags_for_elements( $text_elements );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a list of elements to use semantic heading tags if needed.
|
||||
*
|
||||
* @since 1.18.0
|
||||
*
|
||||
* @param DOMNodeList $text_elements List of text elements.
|
||||
*/
|
||||
private function use_semantic_heading_tags_for_elements( DOMNodeList $text_elements ): void {
|
||||
// Matches PAGE_HEIGHT in the editor, as also seen in amp-story-grid-layer[aspect-ratio].
|
||||
$page_height = 618;
|
||||
|
||||
$has_h1 = false;
|
||||
|
||||
/**
|
||||
* The individual text element.
|
||||
*
|
||||
* @var DOMElement $text_el The text element.
|
||||
*/
|
||||
foreach ( $text_elements as $text_el ) {
|
||||
$style = $text_el->getAttribute( 'style' );
|
||||
$matches = [];
|
||||
|
||||
// See https://github.com/GoogleForCreators/web-stories-wp/issues/10726.
|
||||
if ( \strlen( trim( $text_el->textContent ) ) <= 3 ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! preg_match( '/font-size:([^em]+)em/', $style, $matches ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Contains the font-size in em.
|
||||
// This is basically reversing the dataToFontSizeY() logic. Example:
|
||||
// 0.582524em roughly equals 36 editor pixels: 0.582524 * 618 / 10 = 35.9999px.
|
||||
$font_size_in_em = (float) $matches[1];
|
||||
$font_size_in_px = round( $font_size_in_em * $page_height / 10 );
|
||||
|
||||
if ( $font_size_in_px >= 36 && ! $has_h1 ) {
|
||||
$this->change_tag_name( $text_el, 'h1' );
|
||||
$has_h1 = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $font_size_in_px >= 27 ) {
|
||||
$this->change_tag_name( $text_el, 'h2' );
|
||||
} elseif ( $font_size_in_px >= 21 ) {
|
||||
$this->change_tag_name( $text_el, 'h3' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes an element's tag name.
|
||||
*
|
||||
* @since 1.18.0
|
||||
*
|
||||
* @param DOMElement $node Element whose tag name should be changed.
|
||||
* @param string $tag_name Desired new tag name, e.g. h1 or h2.
|
||||
*/
|
||||
private function change_tag_name( DOMElement $node, string $tag_name ): void {
|
||||
/**
|
||||
* Owner document.
|
||||
*
|
||||
* @var Document|AMP_Document Owner document.
|
||||
*/
|
||||
$document = $node->ownerDocument;
|
||||
|
||||
$new_node = $document->createElement( $tag_name );
|
||||
|
||||
if ( ! $new_node instanceof DOMElement ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy over all children first.
|
||||
foreach ( $node->childNodes as $child ) {
|
||||
/**
|
||||
* Child node.
|
||||
*
|
||||
* @var DOMNode $child Child node.
|
||||
*/
|
||||
|
||||
$new_node->appendChild( $document->importNode( $child->cloneNode( true ), true ) );
|
||||
}
|
||||
|
||||
// Then, copy over all attributes.
|
||||
foreach ( $node->attributes as $attr ) {
|
||||
/**
|
||||
* Attribute.
|
||||
*
|
||||
* @var DOMAttr $attr Attribute.
|
||||
*/
|
||||
$new_node->setAttribute( $attr->nodeName, $attr->nodeValue ?? '' );
|
||||
}
|
||||
|
||||
if ( $node->parentNode ) {
|
||||
$node->parentNode->replaceChild( $new_node, $node );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes <amp-story-page-outlink> elements to ensure they're always valid.
|
||||
*
|
||||
* Removes empty `cta-image` attributes.
|
||||
* Ensures the element is always the last child of <amp-story-page>.
|
||||
*
|
||||
* @since 1.13.0
|
||||
*
|
||||
* @param Document|AMP_Document $document Document instance.
|
||||
*/
|
||||
private function sanitize_amp_story_page_outlink( $document ): void {
|
||||
$outlink_elements = $document->getElementsByTagName( 'amp-story-page-outlink' );
|
||||
|
||||
/**
|
||||
* The <amp-story-page-outlink> element
|
||||
*
|
||||
* @var DOMElement $element The <amp-story-page-outlink> element
|
||||
*/
|
||||
foreach ( $outlink_elements as $element ) {
|
||||
if ( ! $element->getAttribute( 'cta-image' ) ) {
|
||||
$element->removeAttribute( 'cta-image' );
|
||||
}
|
||||
|
||||
$amp_story_page = $element->parentNode;
|
||||
|
||||
if ( $amp_story_page && $element !== $amp_story_page->lastChild ) {
|
||||
$amp_story_page->removeChild( $element );
|
||||
$amp_story_page->appendChild( $element );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the placeholder of publisher logo in the content.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @param Document|AMP_Document $document Document instance.
|
||||
* @param string $publisher_logo Publisher logo.
|
||||
*/
|
||||
private function add_publisher_logo( $document, string $publisher_logo ): void {
|
||||
/**
|
||||
* The <amp-story> element.
|
||||
*
|
||||
* @var DOMElement|DOMNode $story_element The <amp-story> element.
|
||||
*/
|
||||
$story_element = $document->body->getElementsByTagName( 'amp-story' )->item( 0 );
|
||||
|
||||
if ( ! $story_element instanceof DOMElement ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a publisher logo if missing or just a placeholder.
|
||||
$existing_publisher_logo = $story_element->getAttribute( 'publisher-logo-src' );
|
||||
|
||||
// Backward compatibility for when fallback-wordpress-publisher-logo.png was provided by the plugin.
|
||||
if ( ! $existing_publisher_logo || str_contains( $existing_publisher_logo, 'fallback-wordpress-publisher-logo.png' ) ) {
|
||||
$story_element->setAttribute( 'publisher-logo-src', $publisher_logo );
|
||||
}
|
||||
|
||||
if ( ! $story_element->getAttribute( 'publisher-logo-src' ) ) {
|
||||
$story_element->setAttribute( 'publisher-logo-src', $publisher_logo );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the placeholder of publisher in the content.
|
||||
*
|
||||
* Ensures the `publisher` attribute exists if missing.
|
||||
*
|
||||
* @since 1.7.0
|
||||
*
|
||||
* @param Document|AMP_Document $document Document instance.
|
||||
* @param string $publisher Publisher logo.
|
||||
*/
|
||||
private function add_publisher( $document, string $publisher ): void {
|
||||
/**
|
||||
* The <amp-story> element.
|
||||
*
|
||||
* @var DOMElement|DOMNode $story_element The <amp-story> element.
|
||||
*/
|
||||
$story_element = $document->body->getElementsByTagName( 'amp-story' )->item( 0 );
|
||||
|
||||
if ( ! $story_element instanceof DOMElement ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $publisher || ! $story_element->hasAttribute( 'publisher' ) ) {
|
||||
$story_element->setAttribute( 'publisher', $publisher );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds square, and landscape poster images to the <amp-story>.
|
||||
*
|
||||
* @since 1.1.0
|
||||
*
|
||||
* @param Document|AMP_Document $document Document instance.
|
||||
* @param string[] $poster_images List of poster images, keyed by type.
|
||||
*/
|
||||
private function add_poster_images( $document, array $poster_images ): void {
|
||||
/**
|
||||
* The <amp-story> element.
|
||||
*
|
||||
* @var DOMElement|DOMNode $story_element The <amp-story> element.
|
||||
*/
|
||||
$story_element = $document->body->getElementsByTagName( 'amp-story' )->item( 0 );
|
||||
|
||||
if ( ! $story_element instanceof DOMElement ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The story sanitizer only passes valid, non-empty URLs that are already escaped.
|
||||
// That means we don't need to do any additional checks here or worry about accidentally overriding
|
||||
// an existing poster-portrait-src attribute value with an empty one.
|
||||
foreach ( $poster_images as $attr => $url ) {
|
||||
$story_element->setAttribute( $attr, $url );
|
||||
}
|
||||
|
||||
if ( ! $story_element->getAttribute( 'poster-portrait-src' ) ) {
|
||||
$story_element->setAttribute( 'poster-portrait-src', '' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* De-duplicate inline styles in the story.
|
||||
*
|
||||
* Greatly reduce the amount of `style[amp-custom]` CSS for stories by de-duplicating inline styles
|
||||
* and moving to simple class selector style rules, avoiding the specificity hack
|
||||
* that the AMP plugin's style sanitizer employs.
|
||||
*
|
||||
* @since 1.8.0
|
||||
*
|
||||
* @param Document|AMP_Document $document Document instance.
|
||||
*/
|
||||
private function deduplicate_inline_styles( $document ): void {
|
||||
$elements_by_inline_style = [];
|
||||
|
||||
// Gather all elements based on their common inline styles.
|
||||
$elements_with_style = $document->xpath->query( '//*[ @style ]' );
|
||||
|
||||
if ( $elements_with_style instanceof DOMNodeList && $elements_with_style->length > 0 ) {
|
||||
/**
|
||||
* The element with inline styles.
|
||||
*
|
||||
* @var DOMElement $styled_element The element.
|
||||
*/
|
||||
foreach ( $elements_with_style as $styled_element ) {
|
||||
$value = $styled_element->getAttribute( 'style' );
|
||||
$styled_element->removeAttribute( 'style' );
|
||||
$elements_by_inline_style[ $value ][] = $styled_element;
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $elements_by_inline_style ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$style_element = $document->createElement( 'style' );
|
||||
|
||||
if ( ! $style_element ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$document->head->appendChild( $style_element );
|
||||
|
||||
// Create style rule for each inline style and add class name to each element.
|
||||
foreach ( $elements_by_inline_style as $inline_style => $styled_elements ) {
|
||||
$inline_style_class_name = '_' . substr( md5( (string) $inline_style ), 0, 7 );
|
||||
|
||||
$style_rule = $document->createTextNode( sprintf( '.%s{%s}', $inline_style_class_name, $inline_style ) );
|
||||
$style_element->appendChild( $style_rule );
|
||||
|
||||
/**
|
||||
* The element with inline styles.
|
||||
*
|
||||
* @var DOMElement $styled_element The element.
|
||||
*/
|
||||
foreach ( $styled_elements as $styled_element ) {
|
||||
$class_name = $inline_style_class_name;
|
||||
if ( $styled_element->hasAttribute( 'class' ) ) {
|
||||
$class_name .= ' ' . $styled_element->getAttribute( 'class' );
|
||||
}
|
||||
$styled_element->setAttribute( 'class', $class_name );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables using video cache by adding the necessary attribute to `<amp-video>`
|
||||
*
|
||||
* @since 1.10.0
|
||||
*
|
||||
* @param Document|AMP_Document $document Document instance.
|
||||
* @param bool $video_cache_enabled Whether video cache is enabled.
|
||||
*/
|
||||
private function add_video_cache( $document, bool $video_cache_enabled ): void {
|
||||
if ( ! $video_cache_enabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$videos = $document->body->getElementsByTagName( 'amp-video' );
|
||||
|
||||
/**
|
||||
* The <amp-video> element
|
||||
*
|
||||
* @var DOMElement $video The <amp-video> element
|
||||
*/
|
||||
foreach ( $videos as $video ) {
|
||||
$video->setAttribute( 'cache', 'google' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a URL is a `blob:` URL.
|
||||
*
|
||||
* @since 1.9.0
|
||||
*
|
||||
* @param string $url URL.
|
||||
* @return bool Whether it's a blob URL.
|
||||
*/
|
||||
private function is_blob_url( string $url ): bool {
|
||||
return str_starts_with( $url, 'blob:' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove `blob:` URLs from videos and images that might have slipped through.
|
||||
*
|
||||
* @since 1.9.0
|
||||
*
|
||||
* @param Document|AMP_Document $document Document instance.
|
||||
*/
|
||||
private function remove_blob_urls( $document ): void {
|
||||
/**
|
||||
* List of <amp-video> elements.
|
||||
*
|
||||
* @var DOMElement[] $videos Video elements.
|
||||
*/
|
||||
$videos = $document->body->getElementsByTagName( 'amp-video' );
|
||||
|
||||
foreach ( $videos as $video ) {
|
||||
if ( $this->is_blob_url( $video->getAttribute( 'poster' ) ) ) {
|
||||
$video->setAttribute( 'poster', '' );
|
||||
}
|
||||
|
||||
if ( $this->is_blob_url( $video->getAttribute( 'artwork' ) ) ) {
|
||||
$video->setAttribute( 'artwork', '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* List of <source> child elements.
|
||||
*
|
||||
* @var DOMElement[] $video_sources Video source elements.
|
||||
*/
|
||||
$video_sources = $video->getElementsByTagName( 'source' );
|
||||
|
||||
foreach ( $video_sources as $source ) {
|
||||
if ( $this->is_blob_url( $source->getAttribute( 'src' ) ) ) {
|
||||
$source->setAttribute( 'src', '' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List of <amp-img> elements.
|
||||
*
|
||||
* @var DOMElement[] $images Image elements.
|
||||
*/
|
||||
$images = $document->body->getElementsByTagName( 'amp-img' );
|
||||
|
||||
foreach ( $images as $image ) {
|
||||
if ( $this->is_blob_url( $image->getAttribute( 'src' ) ) ) {
|
||||
$image->setAttribute( 'src', '' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize amp-img[srcset] attributes to remove duplicates.
|
||||
*
|
||||
* @since 1.10.0
|
||||
*
|
||||
* @param Document|AMP_Document $document Document instance.
|
||||
*/
|
||||
private function sanitize_srcset( $document ): void {
|
||||
/**
|
||||
* List of <amp-img> elements.
|
||||
*
|
||||
* @var DOMElement[] $images Image elements.
|
||||
*/
|
||||
$images = $document->body->getElementsByTagName( 'amp-img' );
|
||||
|
||||
foreach ( $images as $image ) {
|
||||
$srcset = $image->getAttribute( 'srcset' );
|
||||
if ( ! $srcset ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
|
||||
// Matches every srcset entry (consisting of a URL and a width descriptor) within `srcset=""`.
|
||||
// Not using explode(',') to not break with URLs containing commas.
|
||||
// Given "foo1,2/image.png 123w, foo2,3/image.png 456w", the named capture group "entry"
|
||||
// will contain "foo1,2/image.png 123w" and "foo2,3/image.png 456w", without the trailing commas.
|
||||
preg_match_all( '/((?<entry>[^ ]+ [\d]+w),?)/', $srcset, $matches );
|
||||
|
||||
$entries = $matches['entry'] ?? [];
|
||||
$entries_by_widths = [];
|
||||
|
||||
foreach ( $entries as $entry ) {
|
||||
$entry_data = explode( ' ', $entry );
|
||||
if ( ! isset( $entries_by_widths[ $entry_data[1] ] ) ) {
|
||||
$entries_by_widths[ $entry_data[1] ] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
$image->setAttribute( 'srcset', implode( ', ', $entries_by_widths ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove images referencing the grid-placeholder.png file which has since been removed.
|
||||
*
|
||||
* Prevents 404 errors for non-existent image files when creators forget to replace/remove
|
||||
* the placeholder image.
|
||||
*
|
||||
* The placeholder functionality was removed in v1.14.0, nevertheless older stories could still
|
||||
* reference the files.
|
||||
*
|
||||
* @since 1.14.0
|
||||
*
|
||||
* @link https://github.com/googleforcreators/web-stories-wp/issues/9530
|
||||
*
|
||||
* @param Document|AMP_Document $document Document instance.
|
||||
*/
|
||||
private function remove_page_template_placeholder_images( $document ): void {
|
||||
// Catches "assets/images/editor/grid-placeholder.png" as well as
|
||||
// "web-stories/assets/images/adde98ae406d6b5c95d111a934487252.png" (v1.14.0)
|
||||
// and potentially other variants.
|
||||
$placeholder_img = 'plugins/web-stories/assets/images';
|
||||
|
||||
/**
|
||||
* List of <amp-img> elements.
|
||||
*
|
||||
* @var DOMElement[] $images Image elements.
|
||||
*/
|
||||
$images = $document->body->getElementsByTagName( 'amp-img' );
|
||||
|
||||
foreach ( $images as $image ) {
|
||||
$src = $image->getAttribute( 'src' );
|
||||
|
||||
if ( $image->parentNode && str_contains( $src, $placeholder_img ) ) {
|
||||
$image->parentNode->removeChild( $image );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes <title> tags and meta descriptions.
|
||||
*
|
||||
* Ensures there's always just exactly one of each present.
|
||||
*
|
||||
* @since 1.28.0
|
||||
*
|
||||
* @link https://github.com/googleforcreators/web-stories-wp/issues/12655
|
||||
*
|
||||
* @param Document|AMP_Document $document Document instance.
|
||||
* @param string $title_tag Title text to use if it's missing.
|
||||
* @param string $description Description to use if it's missing.
|
||||
*/
|
||||
private function sanitize_title_and_meta_description( $document, string $title_tag, string $description ): void {
|
||||
/**
|
||||
* List of <title> elements.
|
||||
*
|
||||
* @var DOMNodeList<DOMElement> $titles Title elements.
|
||||
*/
|
||||
$titles = $document->head->getElementsByTagName( 'title' );
|
||||
|
||||
if ( $titles->length > 1 ) {
|
||||
foreach ( $titles as $index => $title ) {
|
||||
if ( 0 === $index ) {
|
||||
continue;
|
||||
}
|
||||
$document->head->removeChild( $title );
|
||||
}
|
||||
}
|
||||
|
||||
if ( 0 === $titles->length && ! empty( $title_tag ) ) {
|
||||
/**
|
||||
* New title tag element.
|
||||
*
|
||||
* @var DOMElement $new_title
|
||||
*/
|
||||
$new_title = $document->createElement( 'title' );
|
||||
|
||||
/**
|
||||
* Title text node.
|
||||
*
|
||||
* @var \DOMText $text_node
|
||||
*/
|
||||
$text_node = $document->createTextNode( $title_tag );
|
||||
|
||||
$new_title->appendChild( $text_node );
|
||||
$document->head->appendChild( $new_title );
|
||||
}
|
||||
|
||||
/**
|
||||
* List of meta descriptions.
|
||||
*
|
||||
* @var DOMNodeList<DOMElement> $meta_descriptions Meta descriptions.
|
||||
*/
|
||||
$meta_descriptions = $document->xpath->query( './/meta[@name="description"]' );
|
||||
|
||||
if ( $meta_descriptions->length > 1 ) {
|
||||
foreach ( $meta_descriptions as $index => $meta_description ) {
|
||||
if ( 0 === $index ) {
|
||||
continue;
|
||||
}
|
||||
$document->head->removeChild( $meta_description );
|
||||
}
|
||||
}
|
||||
|
||||
if ( 0 === $meta_descriptions->length && ! empty( $description ) ) {
|
||||
/**
|
||||
* New meta description element.
|
||||
*
|
||||
* @var DOMElement $new_description
|
||||
*/
|
||||
$new_description = $document->createElement( 'meta' );
|
||||
|
||||
$new_description->setAttribute( 'name', 'description' );
|
||||
$new_description->setAttribute( 'content', $description );
|
||||
$document->head->appendChild( $new_description );
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user