diff --git a/amp.php b/amp.php
index 8e8558f888a..07694de2fef 100644
--- a/amp.php
+++ b/amp.php
@@ -88,6 +88,7 @@ function amp_after_setup_theme() {
}
add_action( 'init', 'amp_init' );
+ add_action( 'rest_api_init', 'amp_rest_init' );
add_action( 'widgets_init', 'AMP_Theme_Support::register_widgets' );
add_action( 'admin_init', 'AMP_Options_Manager::register_settings' );
add_filter( 'amp_post_template_analytics', 'amp_add_custom_analytics' );
@@ -125,6 +126,18 @@ function amp_init() {
if ( class_exists( 'Jetpack' ) && ! ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
require_once( AMP__DIR__ . '/jetpack-helper.php' );
}
+
+ amp_hook_comments_maybe();
+}
+
+/**
+ * Init AMP Rest endpoints.
+ *
+ * @since 0.1
+ */
+function amp_rest_init() {
+ require_once AMP__DIR__ . '/includes/amp-rest-functions.php';
+ amp_register_endpoints();
}
// Make sure the `amp` query var has an explicit value.
@@ -361,3 +374,64 @@ function amp_redirect_old_slug_to_new_url( $link ) {
return $link;
}
+
+/**
+ * Hook into a comment submission if an AMP xhr post request.
+ */
+function amp_hook_comments_maybe() {
+
+ $method = filter_input( INPUT_SERVER, 'REQUEST_METHOD', FILTER_SANITIZE_STRING );
+ $is_amp_submit = filter_input( INPUT_GET, '__amp_source_origin', FILTER_VALIDATE_URL );
+
+ if ( 'post' !== strtolower( $method ) || is_null( $is_amp_submit ) ) {
+ return;
+ }
+
+ // Add amp comment hooks.
+ add_filter( 'comment_post_redirect', 'amp_comment_post_redirect', PHP_INT_MAX, 2 );
+ add_filter( 'wp_die_handler', 'amp_set_comment_submit_die_handler' );
+
+ // Send amp header.
+ header( 'AMP-Access-Control-Allow-Source-Origin: ' . $is_amp_submit, true );
+
+}
+
+/**
+ * Set the die handler on comment submit to output the error in json and send AMP headers.
+ */
+function amp_set_comment_submit_die_handler() {
+ return 'amp_send_error_json';
+}
+
+/**
+ * Send error creating message.
+ *
+ * @param string $message The error message to send.
+ */
+function amp_send_error_json( $message ) {
+ header( 'HTTP/1.1 400 BAD REQUEST' );
+ wp_send_json( array( 'error' => strip_tags( $message, 'strong' ) ) );
+}
+
+/**
+ * Redirect after comment submission.
+ *
+ * @param string $location The location to redirect to.
+ * @param WP_Comment $comment The comment that was posted.
+ * @return string The location URL or void if send json.
+ */
+function amp_comment_post_redirect( $location, $comment ) {
+ $is_amp_submit = filter_input( INPUT_GET, '__amp_source_origin', FILTER_VALIDATE_URL );
+ if ( is_null( $is_amp_submit ) ) {
+ return $location;
+ }
+
+ // AMP-Redirect only works if https. If not, we simply render the success.
+ if ( is_ssl() ) {
+ $location = strtok( $location, '#' );
+ header( 'AMP-Redirect-To: ' . $location );
+ header( 'Access-Control-Expose-Headers: AMP-Redirect-To', false );
+ }
+ $comment->comment_link = get_comment_link( $comment->comment_ID );
+ wp_send_json( $comment );
+}
diff --git a/includes/amp-helper-functions.php b/includes/amp-helper-functions.php
index 70b9fa42f03..7efcd0adc3e 100644
--- a/includes/amp-helper-functions.php
+++ b/includes/amp-helper-functions.php
@@ -222,6 +222,8 @@ function amp_get_content_sanitizers( $post = null ) {
'AMP_Video_Sanitizer' => array(),
'AMP_Audio_Sanitizer' => array(),
'AMP_Playbuzz_Sanitizer' => array(),
+ 'AMP_Form_Sanitizer' => array(),
+ 'AMP_Comments_Sanitizer' => array(),
'AMP_Iframe_Sanitizer' => array(
'add_placeholder' => true,
),
diff --git a/includes/amp-rest-functions.php b/includes/amp-rest-functions.php
new file mode 100644
index 00000000000..a8dc38819a5
--- /dev/null
+++ b/includes/amp-rest-functions.php
@@ -0,0 +1,71 @@
+\d+)', array(
+ 'methods' => 'GET',
+ 'callback' => 'amp_get_comments',
+ ) );
+
+}
+
+/**
+ * Get comments for the Post and Parent
+ *
+ * @since 0.7
+ * @param WP_REST_Request $request The REST request.
+ *
+ * @return array The array of comments.
+ */
+function amp_get_comments( $request ) {
+
+ $id = $request->get_param( 'id' );
+ $comments = get_comments( array(
+ 'post_ID' => $id,
+ 'order' => 'ASC',
+ ) );
+ $return = array(
+ 'items' => array(
+ 'comment' => amp_get_comments_recursive( 0, $comments ),
+ ),
+ );
+
+ return $return;
+}
+
+/**
+ * Recursively get comments for the Post and Parent
+ *
+ * @since 0.7
+ * @param int $parent The comment parent.
+ * @param array $comments The list of comments.
+ *
+ * @return array The array of comments.
+ */
+function amp_get_comments_recursive( $parent, $comments ) {
+
+ $return = array();
+ foreach ( $comments as $comment ) {
+ if ( (int) $parent !== (int) $comment->comment_parent ) {
+ continue;
+ }
+ $GLOBALS['comment'] = $comment; // WPCS: override ok.
+ $comment->comment_date = get_comment_date();
+ $comment->comment_time = get_comment_time();
+ $comment->comment_avatar_url = get_avatar_url( $comment->comment_author_email );
+ $comment->comment = amp_get_comments_recursive( $comment->comment_ID, $comments );
+ $return[] = $comment;
+ }
+
+ return $return;
+}
diff --git a/includes/class-amp-autoloader.php b/includes/class-amp-autoloader.php
index 37264085486..adaa5e1015b 100644
--- a/includes/class-amp-autoloader.php
+++ b/includes/class-amp-autoloader.php
@@ -30,6 +30,7 @@ class AMP_Autoloader {
*/
private static $_classmap = array(
'AMP_Theme_Support' => 'includes/class-amp-theme-support',
+ 'AMP_Comment_Walker' => 'includes/class-amp-comment-walker',
'AMP_WP_Styles' => 'includes/class-amp-wp-styles',
'AMP_Template_Customizer' => 'includes/admin/class-amp-customizer',
'AMP_Post_Meta_Box' => 'includes/admin/class-amp-post-meta-box',
@@ -65,6 +66,8 @@ class AMP_Autoloader {
'AMP_Blacklist_Sanitizer' => 'includes/sanitizers/class-amp-blacklist-sanitizer',
'AMP_Iframe_Sanitizer' => 'includes/sanitizers/class-amp-iframe-sanitizer',
'AMP_Img_Sanitizer' => 'includes/sanitizers/class-amp-img-sanitizer',
+ 'AMP_Form_Sanitizer' => 'includes/sanitizers/class-amp-form-sanitizer',
+ 'AMP_Comments_Sanitizer' => 'includes/sanitizers/class-amp-comments-sanitizer',
'AMP_Playbuzz_Sanitizer' => 'includes/sanitizers/class-amp-playbuzz-sanitizer',
'AMP_Style_Sanitizer' => 'includes/sanitizers/class-amp-style-sanitizer',
'AMP_Tag_And_Attribute_Sanitizer' => 'includes/sanitizers/class-amp-tag-and-attribute-sanitizer',
diff --git a/includes/class-amp-comment-walker.php b/includes/class-amp-comment-walker.php
new file mode 100644
index 00000000000..9b01bd08023
--- /dev/null
+++ b/includes/class-amp-comment-walker.php
@@ -0,0 +1,105 @@
+\n";
+ } else {
+ $output .= "\n";
+ }
+ }
+ $output .= '{{/comment}}';
+ }
+
+ /**
+ * Output amp-list template code and place holder for comments.
+ *
+ * @see Walker::paged_walk()
+ * @param array $elements List of comment Elements.
+ * @param int $max_depth The maximum hierarchical depth.
+ * @param int $page_num The specific page number, beginning with 1.
+ * @param int $per_page Per page counter.
+ *
+ * @return string XHTML of the specified page of elements.
+ */
+ public function paged_walk( $elements, $max_depth, $page_num, $per_page ) {
+ if ( empty( $elements ) || $max_depth < - 1 ) {
+ return '';
+ }
+
+ $args = array_slice( func_get_args(), 4 );
+
+ $url = get_rest_url( get_current_blog_id(), 'amp/v1/comments/' . get_the_ID() );
+ if ( strpos( $url, 'http:' ) === 0 ) {
+ $url = substr( $url, 5 );
+ }
+ // @todo Identify arguments and make filterable/settable.
+ $output = '';
+ $output .= '';
+ $output .= '' . esc_html__( 'Show more', 'amp' ) . '
';
+ $output .= '';
+ $output .= '';
+ $output .= '';
+ $output .= parent::paged_walk( $elements, $max_depth, $page_num, $per_page, $args[0] );
+ $output .= '';
+
+ return $output;
+ }
+}
diff --git a/includes/class-amp-theme-support.php b/includes/class-amp-theme-support.php
index c85bdd9f941..51704aabb29 100644
--- a/includes/class-amp-theme-support.php
+++ b/includes/class-amp-theme-support.php
@@ -26,6 +26,13 @@ class AMP_Theme_Support {
*/
const CUSTOM_STYLES_PLACEHOLDER = '/* AMP:CUSTOM_STYLES_PLACEHOLDER */';
+ /**
+ * Replaced with the comments template.
+ *
+ * @var string
+ */
+ const COMMENTS_TEMPLATE_PLACEHOLDER = '/* AMP:COMMENTS_TEMPLATE_PLACEHOLDER */';
+
/**
* AMP Scripts.
*
@@ -193,6 +200,17 @@ public static function register_hooks() {
*/
add_action( 'template_redirect', array( __CLASS__, 'start_output_buffering' ), 0 );
+ // Add Comments hooks.
+ add_filter( 'wp_list_comments_args', array( __CLASS__, 'add_amp_comments_template' ), PHP_INT_MAX );
+ add_filter( 'comments_array', array( __CLASS__, 'get_comments_template' ) );
+ add_action( 'comment_form_top', array( __CLASS__, 'add_amp_comment_form_templates' ), PHP_INT_MAX );
+ add_action( 'comment_form', array( __CLASS__, 'add_amp_comment_form_templates' ), PHP_INT_MAX );
+ // Filter dynamic data with mustache variable strings.
+ // @todo add additional filters for dynamic data like "get_comment_author_link", "get_comment_author_IP" etc...
+ add_filter( 'get_comment_date', array( __CLASS__, 'get_comment_date_template_string' ), PHP_INT_MAX );
+ add_filter( 'get_comment_time', array( __CLASS__, 'get_comment_time_template_string' ), PHP_INT_MAX );
+ add_filter( 'get_avatar_data', array( __CLASS__, 'get_avatar_data' ), PHP_INT_MAX );
+ add_filter( 'get_comment_ID', array( __CLASS__, 'get_comment_id_template_string' ), PHP_INT_MAX );
// @todo Add character conversion.
}
@@ -343,6 +361,126 @@ public static function add_amp_component_scripts() {
echo self::COMPONENT_SCRIPTS_PLACEHOLDER; // phpcs:ignore WordPress.Security.EscapeOutput, WordPress.XSS.EscapeOutput
}
+ /**
+ * Add the comments template placeholder marker
+ *
+ * @param array $args the args for the comments list..
+ * @return array Args to return.
+ */
+ public static function add_amp_comments_template( $args ) {
+ $amp_walker = new AMP_Comment_Walker();
+ $args['walker'] = $amp_walker;
+
+ return $args;
+ }
+
+ /**
+ * Create a comments_flat array for generating the comment mustache template.
+ *
+ * @return array Flat array of comment placeholders.
+ */
+ public static function get_comments_template() {
+
+ $_comment = array(
+ 'comment_ID' => '{{comment_ID}}',
+ 'comment_post_ID' => get_the_ID(),
+ 'comment_author' => '{{comment_author}}',
+ 'comment_author_email' => '{{comment_author_email}}',
+ 'comment_author_url' => '{{comment_author_url}}',
+ 'comment_author_IP' => '{{comment_author_IP}}',
+ 'comment_date' => '{{comment_date}}',
+ 'comment_date_gmt' => '{{comment_date_gmt}}',
+ 'comment_content' => '{{comment_content}}',
+ 'comment_karma' => '{{comment_karma}}',
+ 'comment_approved' => '{{comment_approved}}',
+ 'comment_agent' => '{{comment_agent}}',
+ 'comment_type' => '{{comment_type}}',
+ 'comment_parent' => '',
+ 'user_id' => '{{user_id}}',
+ );
+
+ $comments = array();
+ $depth = 5;
+ for ( $i = 0; $i < $depth; $i ++ ) {
+ $comment = new stdClass();
+ foreach ( $_comment as $key => $value ) {
+ if ( 'comment_ID' === $key ) {
+ $value = $i + 1;
+ }
+ if ( 'comment_parent' === $key && $i > 0 ) {
+ $value = $i;
+ }
+ $comment->{$key} = $value;
+ }
+ $comments[] = $comment;
+ }
+
+ return $comments;
+
+ }
+
+ /**
+ * Adds the form submit success and fail templates.
+ *
+ * @param string $post_id The post ID if action is 'comment_form'.
+ */
+ public static function add_amp_comment_form_templates( $post_id ) {
+ $output = '';
+ if ( empty( $post_id ) ) {
+ $output .= '
';
+ $output .= '';
+ $output .= esc_html__( 'Your comment has been posted.', 'amp' );
+ $output .= sprintf( '', esc_html__( 'View Comment', 'amp' ) );
+ $output .= '
';
+ $output .= '';
+ $output .= '';
+ $output .= '
';
+ }
+
+ echo $output; // WPCS: XSS OK.
+ }
+
+ /**
+ * Get avatar URL template string.
+ *
+ * @param array $args Avatar data args.
+ * @return array Avatar data with url added.
+ */
+ public static function get_avatar_data( $args ) {
+ $args['url'] = '{{comment_avatar_url}}';
+
+ return $args;
+ }
+
+ /**
+ * Get comment ID string for template.
+ *
+ * @return string Mustache template string.
+ */
+ public static function get_comment_id_template_string() {
+ return '{{comment_ID}}';
+ }
+
+ /**
+ * Get comment date string for template.
+ *
+ * @return string Mustache template string.
+ */
+ public static function get_comment_date_template_string() {
+ return '{{comment_date}}';
+ }
+
+ /**
+ * Get somment time string for template.
+ *
+ * @return string Mustache template string.
+ */
+ public static function get_comment_time_template_string() {
+ return '{{comment_time}}';
+ }
+
/**
* Get canonical URL for current request.
*
@@ -463,6 +601,23 @@ public static function get_amp_custom_styles() {
return $css;
}
+ /**
+ * Determine the type of component script.
+ *
+ * @param string $component The component name.
+ * @return string the type of component.
+ */
+ public static function get_component_type( $component ) {
+ $component_types = apply_filters( 'amp_component_types', array(
+ 'amp-mustache' => 'template',
+ ) );
+
+ if ( ! empty( $component_types[ $component ] ) ) {
+ return $component_types[ $component ];
+ }
+ return 'element';
+ }
+
/**
* Determine required AMP scripts.
*
@@ -493,7 +648,8 @@ public static function get_amp_component_scripts() {
$scripts = '';
foreach ( $amp_scripts as $amp_script_component => $amp_script_source ) {
$scripts .= sprintf(
- '', // phpcs:ignore WordPress.WP.EnqueuedResources, WordPress.XSS.EscapeOutput.OutputNotEscaped
+ '', // phpcs:ignore WordPress.WP.EnqueuedResources, WordPress.XSS.EscapeOutput.OutputNotEscaped
+ self::get_component_type( $amp_script_component ),
$amp_script_component,
$amp_script_source
);
diff --git a/includes/sanitizers/class-amp-comments-sanitizer.php b/includes/sanitizers/class-amp-comments-sanitizer.php
new file mode 100644
index 00000000000..8cbfd5a9f5f
--- /dev/null
+++ b/includes/sanitizers/class-amp-comments-sanitizer.php
@@ -0,0 +1,100 @@
+ tag to identify and process.
+ *
+ * @since 0.2
+ */
+ public static $tag = 'comment-template';
+
+ /**
+ * Sanitize the elements from the HTML contained in this instance's DOMDocument.
+ *
+ * @since 0.2
+ */
+ public function sanitize() {
+
+ /**
+ * Node list.
+ *
+ * @var DOMNodeList $node
+ */
+ $nodes = $this->dom->getElementsByTagName( self::$tag );
+ $num_nodes = $nodes->length;
+
+ if ( 0 === $num_nodes ) {
+ return;
+ }
+
+ for ( $i = 0; $i < $nodes->length; $i++ ) {
+ $node = $nodes->item( $i );
+ if ( ! $node instanceof DOMElement ) {
+ continue;
+ }
+
+ // Get the comment template layer parts. (wrapper, amp-list, template, comments-template).
+ $ol = $node->parentNode;
+ $new_wrap = $node->parentNode->cloneNode();
+ $amp_list = $ol->getElementsByTagName( 'amp-list' )->item( 0 );
+ $amp_template = $ol->getElementsByTagName( 'template' )->item( 0 );
+ if ( ! $amp_list instanceof DOMElement || ! $amp_template instanceof DOMElement ) {
+ continue;
+ }
+
+ // Move Dom parts to an AMP structure.
+ $new_wrap->appendChild( $node );
+ $amp_template->appendChild( $new_wrap );
+ $ol->parentNode->replaceChild( $amp_list, $ol );
+
+ // Convert Links for templating.
+ $links = $amp_template->getElementsByTagName( 'a' );
+ $this->convert_urls( $links, 'href' );
+
+ // Convert image src for templating.
+ $imgs = $amp_template->getElementsByTagName( 'amp-img' );
+ if ( $imgs->length ) {
+ $this->convert_urls( $imgs, 'src' );
+ $this->convert_urls( $imgs, 'srcset' );
+ }
+ }
+
+ // cleanup comments form.
+ $comment_form = $this->dom->getElementById( 'commentform' );
+ if ( $comment_form instanceof DOMElement ) {
+ $comment_form->setAttribute( 'on', 'submit-success:amp-comment-form-fields.hide' );
+ }
+ }
+
+ /**
+ * Convert comment_[field]_url to mustache template strings.
+ *
+ * @since 0.2
+ * @param DOMNodeList $node_list The list of nodes to convert.
+ * @param string $type The type of attribute to convert.
+ */
+ private function convert_urls( $node_list, $type = 'href' ) {
+
+ for ( $a = 0; $a < $node_list->length; $a++ ) {
+ $node = $node_list->item( $a );
+ if ( ! $node->hasAttribute( $type ) ) {
+ continue;
+ }
+ $url = preg_replace( '/http[s]?:\/\/(comment_[a-z_]+_url)/', '{{$1}}', $node->getAttribute( $type ) );
+ $node->setAttribute( $type, $url );
+ }
+ }
+}
diff --git a/includes/sanitizers/class-amp-form-sanitizer.php b/includes/sanitizers/class-amp-form-sanitizer.php
new file mode 100644
index 00000000000..a8aa147cb95
--- /dev/null
+++ b/includes/sanitizers/class-amp-form-sanitizer.php
@@ -0,0 +1,81 @@
+ tag to identify and process.
+ *
+ * @since 0.7
+ */
+ public static $tag = 'form';
+
+ /**
+ * Sanitize the
' . esc_html__( 'Could not load comments.', 'amp' ) . '