OwlCyberSecurity - MANAGER
Edit File: class-amp-comments-sanitizer.php
<?php /** * Class AMP_Comments_Sanitizer. * * @package AMP */ use AmpProject\Amp; use AmpProject\AmpWP\ValidationExemption; use AmpProject\Dom\Element; use AmpProject\Extension; use AmpProject\Html\Attribute; use AmpProject\Html\Tag; /** * Class AMP_Comments_Sanitizer * * Strips and corrects attributes in forms. * * @internal */ class AMP_Comments_Sanitizer extends AMP_Base_Sanitizer { /** @var AMP_Style_Sanitizer|null */ private $style_sanitizer; /** * Default args. * * @since 1.1 * * @var array */ protected $DEFAULT_ARGS = [ 'comments_live_list' => false, 'ampify_comment_threading' => 'always', // Can be 'always', 'never', 'conditionally' (if the comment form was converted to action-xhr). ]; /** * Init. * * @param AMP_Base_Sanitizer[] $sanitizers Sanitizers. */ public function init( $sanitizers ) { if ( array_key_exists( AMP_Style_Sanitizer::class, $sanitizers ) && $sanitizers[ AMP_Style_Sanitizer::class ] instanceof AMP_Style_Sanitizer ) { $this->style_sanitizer = $sanitizers[ AMP_Style_Sanitizer::class ]; } } /** * Pre-process the comment form and comment list for AMP. * * @since 0.7 */ public function sanitize() { // Find the comment form (which may not have the commentform ID). $comment_form = null; foreach ( $this->dom->getElementsByTagName( Tag::FORM ) as $form ) { $action = $form->getAttribute( Attribute::ACTION_XHR ); if ( ! $action ) { $action = $form->getAttribute( Attribute::ACTION ); } $action_path = wp_parse_url( $action, PHP_URL_PATH ); if ( $action_path && 'wp-comments-post.php' === basename( $action_path ) ) { $comment_form = $form; break; } } // Handle the comment reply script. $comment_reply_script = $this->get_comment_reply_script(); $should_ampify_comment_threading = 'always' === $this->args['ampify_comment_threading']; if ( $comment_reply_script instanceof Element ) { if ( 'never' === $this->args['ampify_comment_threading'] ) { $this->prepare_native_comment_reply( $comment_reply_script ); $should_ampify_comment_threading = false; } elseif ( 'always' === $this->args['ampify_comment_threading'] || ( 'conditionally' === $this->args['ampify_comment_threading'] && ( // If there isn't even a comment form on the page, then the comment-reply script shouldn't be here at all. ! $comment_form instanceof Element || // If the form has an action-xhr attribute, then it's going to be an AMP page and we need ampified comment threading. $comment_form->hasAttribute( Attribute::ACTION_XHR ) ) ) ) { // Remove the script and then proceed with the amp-bind implementation below. $comment_reply_script->parentNode->removeChild( $comment_reply_script ); $should_ampify_comment_threading = true; } else { // This is the conditionally-no case. $this->prepare_native_comment_reply( $comment_reply_script ); // Do not proceed with the AMP-bind implementation for threaded comments since the comment-reply script was included. $should_ampify_comment_threading = false; } } // Now based on the comment reply script handling above, ampify the comment form. if ( get_option( 'thread_comments' ) && $comment_form && $should_ampify_comment_threading ) { $this->ampify_threaded_comments( $comment_form ); } if ( $this->args['comments_live_list'] ) { $comments = $this->dom->xpath->query( '//amp-live-list/*[ @items ]/*[ starts-with( @id, "comment-" ) ]' ); foreach ( $comments as $comment ) { $this->add_amp_live_list_comment_attributes( $comment ); } } } /** * Get comment reply script. * * @return Element|null */ protected function get_comment_reply_script() { $element = $this->dom->getElementById( 'comment-reply-js' ); if ( $element instanceof Element && Tag::SCRIPT === $element->tagName ) { return $element; } else { return null; } } /** * Ampify threaded comments by utilizing amp-bind to implement comment reply functionality. * * @param Element $comment_form Comment form. */ protected function ampify_threaded_comments( Element $comment_form ) { // Create reply state. $amp_state = $this->dom->createElement( Extension::STATE ); $comment_form->insertBefore( $amp_state, $comment_form->firstChild ); $state_id = 'ampCommentThreading'; $amp_state->setAttribute( Attribute::ID, $state_id ); $state = [ 'replyTo' => '', 'commentParent' => '0', // @todo What if page accessed with replytocom? Then this should be $comment_parent_id below. ]; $comment_parent_id = 0; $comment_parent_input = $this->dom->getElementById( 'comment_parent' ); if ( $comment_parent_input instanceof Element ) { $comment_parent_id = (int) $comment_parent_input->getAttribute( Attribute::VALUE ); $comment_parent_input->setAttribute( Amp::BIND_DATA_ATTR_PREFIX . 'value', sprintf( '%s.commentParent', $state_id ) ); } // Add amp-state to the document. $script = $this->dom->createElement( Tag::SCRIPT ); $script->setAttribute( Attribute::TYPE, 'application/json' ); $script->appendChild( $this->dom->createTextNode( wp_json_encode( $state, JSON_UNESCAPED_UNICODE ) ) ); $amp_state->appendChild( $script ); // @todo This should also remove the novalidate attribute from the form. // Reset state when submitting form. $comment_form->addAmpAction( 'submit-success', sprintf( '%s.clear,AMP.setState({%s: %s})', $this->dom->getElementId( $comment_form ), $state_id, wp_json_encode( $state ) ) ); // Prepare the comment form for replies. The logic here corresponds to what is found in comment-reply.js. $reply_heading_element = $this->dom->getElementById( 'reply-title' ); $reply_heading_text_node = null; // The text node for the heading. if ( $reply_heading_element && $reply_heading_element->firstChild instanceof DOMText ) { $reply_heading_text_node = $reply_heading_element->firstChild; } // Add the text binding to the heading text, which necessitates wrapping in a span. if ( $reply_heading_text_node ) { $reply_heading_text_span = $this->dom->createElement( Tag::SPAN ); $reply_heading_element->replaceChild( $reply_heading_text_span, $reply_heading_text_node ); $reply_heading_text_span->appendChild( $reply_heading_text_node ); // Move whitespace after the node. $reply_heading_text_node->nodeValue = rtrim( $reply_heading_text_node->nodeValue ); $reply_heading_element->insertBefore( $this->dom->createTextNode( ' ' ), $reply_heading_text_span->nextSibling ); // Note: if the replytocom query parameter was set, then the existing value will already be a replyTo value. // Nevertheless, the link will have the replytocom arg removed, so clicking on the link will cause // navigation to a page that has the nameless heading text. $text_binding = sprintf( '%1$s.replyTo ? %1$s.replyTo : %2$s', $state_id, wp_json_encode( $reply_heading_text_node->nodeValue, JSON_UNESCAPED_UNICODE ) ); $reply_heading_text_span->setAttribute( Amp::BIND_DATA_ATTR_PREFIX . 'text', $text_binding ); } // Update comment reply links to set the reply state. $comment_reply_links = $this->dom->xpath->query( '//a[ @data-commentid and @data-postid and @data-respondelement and contains( @class, "comment-reply-link" ) ]' ); foreach ( $comment_reply_links as $comment_reply_link ) { /** @var Element $comment_reply_link */ $comment_reply_state = [ $state_id => [ 'replyTo' => $comment_reply_link->getAttribute( 'data-replyto' ), 'commentParent' => $comment_reply_link->getAttribute( 'data-commentid' ), ], ]; $comment_reply_link->setAttribute( Attribute::HREF, '#' . $comment_reply_link->getAttribute( 'data-respondelement' ) ); $comment_reply_link->addAmpAction( 'tap', sprintf( 'AMP.setState(%s),comment.focus', wp_json_encode( $comment_reply_state, JSON_UNESCAPED_UNICODE ) ) ); } $cancel_comment_reply_link = $this->dom->getElementById( 'cancel-comment-reply-link' ); if ( $cancel_comment_reply_link instanceof Element ) { // Use hidden attribute to hide/show cancel reply link when commentParent is zero. $cancel_comment_reply_link->removeAttribute( Attribute::STYLE ); if ( ! $comment_parent_id ) { $cancel_comment_reply_link->setAttributeNode( $this->dom->createAttribute( Attribute::HIDDEN ) ); } $cancel_comment_reply_link->setAttribute( Amp::BIND_DATA_ATTR_PREFIX . Attribute::HIDDEN, sprintf( '%s.commentParent == "0"', $state_id ) ); // Reset state when clicking cancel. $cancel_comment_reply_link->addAmpAction( 'tap', sprintf( 'AMP.setState({%s: %s})', $state_id, wp_json_encode( $state, JSON_UNESCAPED_UNICODE ) ) ); } } /** * Prepare for native comment-reply functionality. * * @param Element $comment_reply_script Comment reply script. */ protected function prepare_native_comment_reply( Element $comment_reply_script ) { // Mark the comment-reply script as being PX-verified, which was not done in the script sanitizer because // we had to wait until after the form sanitizer ran to find out if we could conditionally serve valid AMP. ValidationExemption::mark_node_as_px_verified( $comment_reply_script ); $comment_reply_script->setAttributeNode( $this->dom->createAttribute( Attribute::DEFER ) ); // Make sure that that inline styles are not transformed or else they will break comment-reply styling. if ( $this->style_sanitizer ) { $this->style_sanitizer->update_args( [ 'transform_important_qualifiers' => false ] ); } } /** * Add attributes to comment elements when comments are being presented in amp-live-list, when comments_live_list theme support flag is present. * * @since 1.1 * * @param Element $comment_element Comment element. */ protected function add_amp_live_list_comment_attributes( $comment_element ) { $comment_id = (int) str_replace( 'comment-', '', $comment_element->getAttribute( 'id' ) ); if ( ! $comment_id ) { return; } $comment_object = get_comment( $comment_id ); // Skip if the comment is not valid or the comment has a parent, since in that case it is not relevant for amp-live-list. if ( ! ( $comment_object instanceof WP_Comment ) || $comment_object->comment_parent ) { return; } $comment_element->setAttribute( 'data-sort-time', strtotime( $comment_object->comment_date ) ); $update_time = strtotime( $comment_object->comment_date ); // Ensure the top-level data-update-time reflects the max time of the comments in the thread. $children = $comment_object->get_children( [ 'format' => 'flat', 'hierarchical' => 'flat', 'orderby' => 'none', ] ); foreach ( $children as $child_comment ) { $update_time = max( strtotime( $child_comment->comment_date ), $update_time ); } $comment_element->setAttribute( 'data-update-time', $update_time ); } }