|
Server : LiteSpeed System : Linux host 5.15.0-161-generic #171-Ubuntu SMP Sat Oct 11 08:17:01 UTC 2025 x86_64 User : idnco5810 ( 1093) PHP Version : 8.2.29 Disable Function : NONE Directory : /home/idn98.co/public_html/wp-content/plugins/amp/includes/sanitizers/ |
Upload File : |
<?php
/**
* Class AMP_Links_Sanitizer.
*
* @package AMP
*/
use AmpProject\Dom\Document;
use AmpProject\Html\Attribute;
use AmpProject\Html\Tag;
/**
* Class AMP_Link_Sanitizer.
*
* Adapts links for AMP-to-AMP navigation:
* - In paired AMP (Transitional and Reader modes), internal links get '?amp' added to them.
* - Internal links on AMP pages get rel=amphtml added to them.
* - Forms with internal actions get a hidden 'amp' input added to them.
* - AMP pages get meta[amp-to-amp-navigation] added to them.
* - Any elements in the admin bar are excluded.
*
* Adapted from https://gist.github.com/westonruter/f9ee9ea717d52471bae092879e3d52b0
*
* @link https://github.com/ampproject/amphtml/issues/12496
* @since 1.4.0
* @internal
*/
class AMP_Link_Sanitizer extends AMP_Base_Sanitizer {
/**
* Default meta tag content.
*
* @var string
*/
const DEFAULT_META_CONTENT = 'AMP-Redirect-To; AMP.navigateTo';
/**
* Placeholder for default arguments, to be set in child classes.
*
* @var array
*/
protected $DEFAULT_ARGS = [ // phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase
'paired' => false, // Only set to true when in a paired mode (will be false when amp_is_canonical()). Controls whether query var is added.
'meta_content' => self::DEFAULT_META_CONTENT,
'excluded_urls' => [], // URLs in this won't have AMP-to-AMP links in a paired mode.
];
/**
* Home host.
*
* @var string
*/
protected $home_host;
/**
* Home path.
*
* @var string
*/
protected $home_path;
/**
* Content path.
*
* @var string
*/
protected $content_path;
/**
* Admin path.
*
* @var string
*/
protected $admin_path;
/**
* Sanitizer constructor.
*
* @param Document $dom Document.
* @param array $args Args.
*/
public function __construct( $dom, array $args = [] ) {
if ( ! isset( $args['meta_content'] ) ) {
$args['meta_content'] = self::DEFAULT_META_CONTENT;
}
parent::__construct( $dom, $args );
$parsed_home = wp_parse_url( home_url( '/' ) );
$this->home_host = $parsed_home['host'] ?? null;
$this->home_path = $parsed_home['path'] ?? '/';
$this->content_path = wp_parse_url( content_url( '/' ), PHP_URL_PATH );
$this->admin_path = wp_parse_url( admin_url(), PHP_URL_PATH );
}
/**
* Sanitize.
*/
public function sanitize() {
if ( ! empty( $this->args['meta_content'] ) ) {
$this->add_meta_tag( $this->args['meta_content'] );
}
$this->process_links();
}
/**
* Add the amp-to-amp-navigation meta tag.
*
* @param string $content The content for the meta tag, for example 'AMP-Redirect-To; AMP.navigateTo'.
* @return DOMElement|null The added meta element if successful.
*/
public function add_meta_tag( $content = self::DEFAULT_META_CONTENT ) {
if ( ! $content ) {
return null;
}
$meta = $this->dom->createElement( 'meta' );
$meta->setAttribute( 'name', 'amp-to-amp-navigation' );
$meta->setAttribute( 'content', $content );
$this->dom->head->appendChild( $meta );
return $meta;
}
/**
* Process links by adding AMP query var to links in paired mode and adding rel=amphtml.
*/
public function process_links() {
// Remove admin bar from DOM to prevent mutating it.
/** @var DOMElement|null */
$admin_bar_container = $this->dom->getElementById( 'wpadminbar' );
/** @var DOMComment|null */
$admin_bar_placeholder = null;
if ( $admin_bar_container ) {
$admin_bar_placeholder = $this->dom->createComment( 'wpadminbar' );
$admin_bar_container->parentNode->replaceChild( $admin_bar_placeholder, $admin_bar_container );
}
$link_query = $this->dom->xpath->query( '//*[ local-name() = "a" or local-name() = "area" ][ @href ]' );
foreach ( $link_query as $link ) {
$this->process_element( $link, Attribute::HREF );
}
$form_query = $this->dom->xpath->query(
'
//form[
@action
and
(
not( @method )
or
translate( @method, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") = "get"
)
]
'
);
foreach ( $form_query as $form ) {
$this->process_element( $form, Attribute::ACTION );
}
// Replace the admin bar after mutations are done.
if ( $admin_bar_container && $admin_bar_placeholder ) {
$admin_bar_placeholder->parentNode->replaceChild( $admin_bar_container, $admin_bar_placeholder );
}
}
/**
* Check if element is descendant of a template element.
*
* @param DOMElement $node Node.
* @return bool Descendant of template.
*/
private function is_descendant_of_template_element( DOMElement $node ) {
while ( $node instanceof DOMElement ) {
$parent = $node->parentNode;
if ( $parent instanceof DOMElement && Tag::TEMPLATE === $parent->tagName ) {
return true;
}
$node = $parent;
}
return false;
}
/**
* Process element.
*
* @param DOMElement $element Element to process.
* @param string $attribute_name Attribute name that contains the URL.
*/
private function process_element( DOMElement $element, $attribute_name ) {
$url = $element->getAttribute( $attribute_name );
// Skip page anchor links or non-frontend links.
if ( empty( $url ) || '#' === substr( $url, 0, 1 ) || ! $this->is_frontend_url( $url ) ) {
return;
}
// Skip links with template variables.
if ( preg_match( '/{{[^}]+?}}/', $url ) && $this->is_descendant_of_template_element( $element ) ) {
return;
}
/** @var array */
$rel = [];
// Gather the rel values that were attributed to the element.
// Note that links and forms may both have this attribute.
// See <https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel>.
if ( $element->hasAttribute( Attribute::REL ) ) {
$rel = array_filter( preg_split( '/\s+/', trim( $element->getAttribute( Attribute::REL ) ) ) );
}
$excluded = (
in_array( Attribute::REL_NOAMPHTML, $rel, true )
||
in_array( strtok( $url, '#' ), $this->args['excluded_urls'], true )
);
/**
* Filters whether AMP-to-AMP is excluded for an element.
*
* The element may be either a link (`a` or `area`) or a `form`.
*
* @param bool $excluded Excluded. Default value is whether element already has a `noamphtml` link relation or the URL is among `excluded_urls`.
* @param string $url URL considered for exclusion.
* @param string[] $rel Link relations.
* @param DOMElement $element The element considered for excluding from AMP-to-AMP linking. May be instance of `a`, `area`, or `form`.
*/
$excluded = (bool) apply_filters( 'amp_to_amp_linking_element_excluded', $excluded, $url, $rel, $element );
$query_vars = [];
if ( ! $excluded ) {
$rel = array_diff(
$rel,
[ Attribute::REL_NOAMPHTML ]
);
if ( ! empty( $rel ) ) {
$element->setAttribute( Attribute::REL, implode( ' ', $rel ) );
} else {
$element->removeAttribute( Attribute::REL );
}
}
/**
* Filters the query vars that are added to the link/form which is considered for AMP-to-AMP linking.
*
* @internal
*
* @param string[] $query_vars Query vars.
* @param bool $excluded Whether the element was excluded.
* @param string $url URL considered for exclusion.
* @param string[] $rel Link relations.
* @param DOMElement $element Element.
*/
$query_vars = apply_filters( 'amp_to_amp_linking_element_query_vars', $query_vars, $excluded, $url, $element, $rel );
if ( ! empty( $query_vars ) ) {
$url = add_query_arg( $query_vars, $url );
}
// Only add the AMP query var when requested (in Transitional or Reader mode).
if ( ! $excluded && ! empty( $this->args['paired'] ) ) {
$url = amp_add_paired_endpoint( $url );
}
$element->setAttribute( $attribute_name, $url );
// Given that form action query vars get overridden by the inputs, they need to be extracted and added as inputs.
if ( Tag::FORM === $element->nodeName ) {
$query = wp_parse_url( $url, PHP_URL_QUERY );
if ( $query ) {
$parsed_query_vars = [];
wp_parse_str( $query, $parsed_query_vars );
$query_vars = array_merge( $query_vars, $parsed_query_vars );
}
foreach ( $query_vars as $name => $value ) {
$input = $this->dom->createElement( Tag::INPUT );
$input->setAttribute( Attribute::NAME, $name );
$input->setAttribute( Attribute::VALUE, $value );
$input->setAttribute( Attribute::TYPE, 'hidden' );
$element->appendChild( $input );
}
}
}
/**
* Determine whether a URL is for the frontend.
*
* @param string $url URL.
* @return bool Whether it is a frontend URL.
*/
public function is_frontend_url( $url ) {
$parsed_url = wp_parse_url( $url );
if ( ! empty( $parsed_url['scheme'] ) && ! in_array( strtolower( $parsed_url['scheme'] ), [ 'http', 'https' ], true ) ) {
return false;
}
// Skip adding query var to links on other URLs.
if ( ! empty( $parsed_url['host'] ) && $this->home_host !== $parsed_url['host'] ) {
return false;
}
// Skip adding query var to links on other paths.
if ( ! empty( $parsed_url['path'] ) && 0 !== strpos( $parsed_url['path'], $this->home_path ) ) {
return false;
}
// Skip adding query var to PHP files (e.g. wp-login.php).
if ( ! empty( $parsed_url['path'] ) && preg_match( '/\.php$/', $parsed_url['path'] ) ) {
return false;
}
// Skip adding query var to feed URLs.
if ( ! empty( $parsed_url['path'] ) && preg_match( ':/feed/(\w+/)?$:', $parsed_url['path'] ) ) {
return false;
}
// Skip adding query var to the admin.
if ( ! empty( $parsed_url['path'] ) && false !== strpos( $parsed_url['path'], $this->admin_path ) ) {
return false;
}
// Skip adding query var to content links (e.g. images).
if ( ! empty( $parsed_url['path'] ) && false !== strpos( $parsed_url['path'], $this->content_path ) ) {
return false;
}
return true;
}
}