diff --git a/.dev-lib b/.dev-lib
index 25f81df2221..6549fa32c46 100644
--- a/.dev-lib
+++ b/.dev-lib
@@ -1,6 +1,7 @@
PATH_EXCLUDES_PATTERN=includes/lib/
DEFAULT_BASE_BRANCH=develop
ASSETS_DIR=wp-assets
+PROJECT_SLUG=amp
function after_wp_install {
echo "Installing REST API..."
diff --git a/amp.php b/amp.php
index 0e59906c557..e9c3c730c22 100644
--- a/amp.php
+++ b/amp.php
@@ -109,9 +109,9 @@ function amp_init() {
add_rewrite_endpoint( AMP_QUERY_VAR, EP_PERMALINK );
+ AMP_Validation_Utils::init();
AMP_Theme_Support::init();
AMP_Post_Type_Support::add_post_type_support();
- AMP_Validation_Utils::init();
add_filter( 'request', 'amp_force_query_var_value' );
add_action( 'admin_init', 'AMP_Options_Manager::register_settings' );
add_action( 'wp_loaded', 'amp_post_meta_box' );
diff --git a/includes/amp-helper-functions.php b/includes/amp-helper-functions.php
index a7c2331487d..be8fd0e3619 100644
--- a/includes/amp-helper-functions.php
+++ b/includes/amp-helper-functions.php
@@ -32,12 +32,16 @@ function amp_get_permalink( $post_id ) {
return $pre_url;
}
- $parsed_url = wp_parse_url( get_permalink( $post_id ) );
- $structure = get_option( 'permalink_structure' );
- if ( empty( $structure ) || ! empty( $parsed_url['query'] ) || is_post_type_hierarchical( get_post_type( $post_id ) ) ) {
- $amp_url = add_query_arg( AMP_QUERY_VAR, '', get_permalink( $post_id ) );
+ if ( amp_is_canonical() ) {
+ $amp_url = get_permalink( $post_id );
} else {
- $amp_url = trailingslashit( get_permalink( $post_id ) ) . user_trailingslashit( AMP_QUERY_VAR, 'single_amp' );
+ $parsed_url = wp_parse_url( get_permalink( $post_id ) );
+ $structure = get_option( 'permalink_structure' );
+ if ( empty( $structure ) || ! empty( $parsed_url['query'] ) || is_post_type_hierarchical( get_post_type( $post_id ) ) ) {
+ $amp_url = add_query_arg( AMP_QUERY_VAR, '', get_permalink( $post_id ) );
+ } else {
+ $amp_url = trailingslashit( get_permalink( $post_id ) ) . user_trailingslashit( AMP_QUERY_VAR, 'single_amp' );
+ }
}
/**
@@ -51,6 +55,25 @@ function amp_get_permalink( $post_id ) {
return apply_filters( 'amp_get_permalink', $amp_url, $post_id );
}
+/**
+ * Remove the AMP endpoint (and query var) from a given URL.
+ *
+ * @since 0.7
+ *
+ * @param string $url URL.
+ * @return string URL with AMP stripped.
+ */
+function amp_remove_endpoint( $url ) {
+
+ // Strip endpoint.
+ $url = preg_replace( ':/' . preg_quote( AMP_QUERY_VAR, ':' ) . '(?=/?(\?|#|$)):', '', $url );
+
+ // Strip query var.
+ $url = remove_query_arg( AMP_QUERY_VAR, $url );
+
+ return $url;
+}
+
/**
* Determine whether a given post supports AMP.
*
diff --git a/includes/class-amp-theme-support.php b/includes/class-amp-theme-support.php
index 2bee8dc416e..ee7da221ad0 100644
--- a/includes/class-amp-theme-support.php
+++ b/includes/class-amp-theme-support.php
@@ -77,13 +77,7 @@ public static function init() {
self::purge_amp_query_vars();
self::handle_xhr_request();
-
- if ( ! is_amp_endpoint() ) {
- amp_add_frontend_actions();
- } else {
- self::setup_commenting();
- add_action( 'widgets_init', array( __CLASS__, 'register_widgets' ) );
- }
+ self::add_temporary_discussion_restrictions();
require_once AMP__DIR__ . '/includes/amp-post-template-actions.php';
@@ -98,20 +92,55 @@ public static function init() {
}
}
- if ( amp_is_canonical() ) {
+ add_action( 'widgets_init', array( __CLASS__, 'register_widgets' ) );
- // Redirect to canonical URL if the AMP URL was loaded, since canonical is now AMP.
- if ( false !== get_query_var( AMP_QUERY_VAR, false ) ) { // Because is_amp_endpoint() now returns true if amp_is_canonical().
- wp_safe_redirect( self::get_current_canonical_url(), 302 ); // Temporary redirect because canonical may change in future.
- exit;
- }
+ /*
+ * Note that wp action is use instead of template_redirect because some themes/plugins output
+ * the response at this action and then short-circuit with exit. So this is why the the preceding
+ * action to template_redirect--the wp action--is used instead.
+ */
+ add_action( 'wp', array( __CLASS__, 'finish_init' ), PHP_INT_MAX );
+ }
+
+ /**
+ * Finish initialization once query vars are set.
+ *
+ * @since 0.7
+ */
+ public static function finish_init() {
+ if ( ! is_amp_endpoint() ) {
+ amp_add_frontend_actions();
+ return;
+ }
+
+ if ( amp_is_canonical() ) {
+ self::redirect_canonical_amp();
} else {
self::register_paired_hooks();
}
- self::register_hooks();
- self::$embed_handlers = self::register_content_embed_handlers();
+ self::add_hooks();
self::$sanitizer_classes = amp_get_content_sanitizers();
+ self::$embed_handlers = self::register_content_embed_handlers();
+ }
+
+ /**
+ * Redirect to canonical URL if the AMP URL was loaded, since canonical is now AMP.
+ *
+ * @since 0.7
+ */
+ public static function redirect_canonical_amp() {
+ if ( false !== get_query_var( AMP_QUERY_VAR, false ) ) { // Because is_amp_endpoint() now returns true if amp_is_canonical().
+ $url = preg_replace( '#^(https?://.+?)(/.*)$#', '$1', home_url( '/' ) );
+ if ( isset( $_SERVER['REQUEST_URI'] ) ) {
+ $url .= wp_unslash( $_SERVER['REQUEST_URI'] );
+ }
+
+ $url = amp_remove_endpoint( $url );
+
+ wp_safe_redirect( $url, 302 ); // Temporary redirect because canonical may change in future.
+ exit;
+ }
}
/**
@@ -166,7 +195,7 @@ public static function register_paired_hooks() {
/**
* Register hooks.
*/
- public static function register_hooks() {
+ public static function add_hooks() {
// Remove core actions which are invalid AMP.
remove_action( 'wp_head', 'wp_post_preview_js', 1 );
@@ -216,6 +245,10 @@ public static function register_hooks() {
add_action( 'comment_form', array( __CLASS__, 'add_amp_comment_form_templates' ), 100 );
remove_action( 'comment_form', 'wp_comment_form_unfiltered_html_nonce' );
+ if ( AMP_Validation_Utils::should_validate_response() ) {
+ AMP_Validation_Utils::add_validation_hooks();
+ }
+
// @todo Add character conversion.
}
@@ -279,7 +312,7 @@ public static function purge_amp_query_vars() {
}
/**
- * Hook into a form submissions, such as comment the form or some other .
+ * Hook into a form submissions, such as the comment form or some other form submission.
*
* @since 0.7.0
* @global string $pagenow
@@ -370,14 +403,14 @@ public static function intercept_post_request_redirect( $location ) {
}
/**
- * Set up commenting.
+ * Set up some restrictions for commenting based on amp-live-list limitations.
+ *
+ * Temporarily force comments to be listed in descending order.
+ * The following hooks are temporary while waiting for amphtml#5396 to be resolved.
+ *
+ * @link https://github.com/ampproject/amphtml/issues/5396
*/
- public static function setup_commenting() {
- /*
- * Temporarily force comments to be listed in descending order.
- *
- * The following hooks are temporary while waiting for amphtml#5396 to be resolved.
- */
+ protected static function add_temporary_discussion_restrictions() {
add_filter( 'option_comment_order', function() {
return 'desc';
}, PHP_INT_MAX );
@@ -584,13 +617,7 @@ public static function get_current_canonical_url() {
$url = add_query_arg( $added_query_vars, $url );
}
- // Strip endpoint.
- $url = preg_replace( ':/' . preg_quote( AMP_QUERY_VAR, ':' ) . '(?=/?(\?|#|$)):', '', $url );
-
- // Strip query var.
- $url = remove_query_arg( AMP_QUERY_VAR, $url );
-
- return $url;
+ return amp_remove_endpoint( $url );
}
/**
@@ -953,13 +980,15 @@ public static function prepare_response( $response, $args = array() ) {
return $response;
}
+ $is_validation_debug_mode = ! empty( $_REQUEST[ AMP_Validation_Utils::DEBUG_QUERY_VAR ] ); // WPCS: csrf ok.
+
$args = array_merge(
array(
'content_max_width' => ! empty( $content_width ) ? $content_width : AMP_Post_Template::CONTENT_MAX_WIDTH, // Back-compat.
'use_document_element' => true,
- 'remove_invalid_callback' => null,
'allow_dirty_styles' => self::is_customize_preview_iframe(), // Dirty styles only needed when editing (e.g. for edit shortcodes).
'allow_dirty_scripts' => is_customize_preview(), // Scripts are always needed to inject changeset UUID.
+ 'disable_invalid_removal' => $is_validation_debug_mode,
),
$args
);
@@ -994,6 +1023,12 @@ public static function prepare_response( $response, $args = array() ) {
trigger_error( esc_html( sprintf( __( 'The database has the %s encoding when it needs to be utf-8 to work with AMP.', 'amp' ), get_bloginfo( 'charset' ) ) ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
}
+ if ( AMP_Validation_Utils::should_validate_response() ) {
+ AMP_Validation_Utils::finalize_validation( $dom, array(
+ 'remove_source_comments' => ! $is_validation_debug_mode,
+ ) );
+ }
+
$response = "\n";
$response .= AMP_DOM_Utils::get_content_from_dom_node( $dom, $dom->documentElement );
diff --git a/includes/options/class-amp-options-menu.php b/includes/options/class-amp-options-menu.php
index a06d5f4169a..93e51e69c0b 100644
--- a/includes/options/class-amp-options-menu.php
+++ b/includes/options/class-amp-options-menu.php
@@ -22,7 +22,7 @@ class AMP_Options_Menu {
*/
public function init() {
add_action( 'admin_post_amp_analytics_options', 'AMP_Options_Manager::handle_analytics_submit' );
- add_action( 'admin_menu', array( $this, 'add_menu_items' ) );
+ add_action( 'admin_menu', array( $this, 'add_menu_items' ), 9 );
}
/**
diff --git a/includes/sanitizers/class-amp-base-sanitizer.php b/includes/sanitizers/class-amp-base-sanitizer.php
index de5c08c8881..5f939c53320 100644
--- a/includes/sanitizers/class-amp-base-sanitizer.php
+++ b/includes/sanitizers/class-amp-base-sanitizer.php
@@ -54,6 +54,7 @@ abstract class AMP_Base_Sanitizer {
* @type array $amp_bind_placeholder_prefix
* @type bool $allow_dirty_styles
* @type bool $allow_dirty_scripts
+ * @type bool $disable_invalid_removal
* @type callable $remove_invalid_callback
* }
*/
@@ -320,17 +321,19 @@ public function maybe_enforce_https_src( $src, $force_https = false ) {
*
* @since 0.7
*
- * @param DOMNode|DOMElement $child The node to remove.
+ * @param DOMNode|DOMElement $node The node to remove.
+ * @param array $args Additional args to pass to validation error callback.
+ *
* @return void
*/
- public function remove_invalid_child( $child ) {
- $parent = $child->parentNode;
- $child->parentNode->removeChild( $child );
- if ( isset( $this->args['remove_invalid_callback'] ) ) {
- call_user_func( $this->args['remove_invalid_callback'], array(
- 'node' => $child,
- 'parent' => $parent,
- ) );
+ public function remove_invalid_child( $node, $args = array() ) {
+ if ( isset( $this->args['validation_error_callback'] ) ) {
+ call_user_func( $this->args['validation_error_callback'],
+ array_merge( compact( 'node' ), $args )
+ );
+ }
+ if ( empty( $this->args['disable_invalid_removal'] ) ) {
+ $node->parentNode->removeChild( $node );
}
}
@@ -344,25 +347,33 @@ public function remove_invalid_child( $child ) {
*
* @param DOMElement $element The node for which to remove the attribute.
* @param DOMAttr|string $attribute The attribute to remove from the element.
+ * @param array $args Additional args to pass to validation error callback.
* @return void
*/
- public function remove_invalid_attribute( $element, $attribute ) {
- if ( isset( $this->args['remove_invalid_callback'] ) ) {
+ public function remove_invalid_attribute( $element, $attribute, $args = array() ) {
+ if ( isset( $this->args['validation_error_callback'] ) ) {
if ( is_string( $attribute ) ) {
$attribute = $element->getAttributeNode( $attribute );
}
if ( $attribute ) {
+ call_user_func( $this->args['validation_error_callback'],
+ array_merge(
+ array(
+ 'node' => $attribute,
+ ),
+ $args
+ )
+ );
+ if ( empty( $this->args['disable_invalid_removal'] ) ) {
+ $element->removeAttributeNode( $attribute );
+ }
+ }
+ } elseif ( empty( $this->args['disable_invalid_removal'] ) ) {
+ if ( is_string( $attribute ) ) {
+ $element->removeAttribute( $attribute );
+ } else {
$element->removeAttributeNode( $attribute );
- call_user_func( $this->args['remove_invalid_callback'], array(
- 'node' => $attribute,
- 'parent' => $element,
- ) );
}
- } elseif ( is_string( $attribute ) ) {
- $element->removeAttribute( $attribute );
- } else {
- $element->removeAttributeNode( $attribute );
}
}
-
}
diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php
index f085144cf87..d02257083a1 100644
--- a/includes/sanitizers/class-amp-style-sanitizer.php
+++ b/includes/sanitizers/class-amp-style-sanitizer.php
@@ -15,9 +15,10 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer {
/**
* Styles.
*
- * @var string[] List of CSS styles in HTML content of DOMDocument ($this->dom).
+ * List of CSS styles in HTML content of DOMDocument ($this->dom).
*
* @since 0.4
+ * @var array[]
*/
private $styles = array();
@@ -47,6 +48,15 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer {
*/
private $custom_max_size;
+ /**
+ * Current CSS size.
+ *
+ * Sum of CSS located in $styles and $stylesheets.
+ *
+ * @var int
+ */
+ private $current_custom_size = 0;
+
/**
* The style[amp-custom] element.
*
@@ -125,7 +135,7 @@ public function __construct( DOMDocument $dom, array $args = array() ) {
*
* @since 0.4
*
- * @return string[] Mapping CSS selectors to array of properties, or mapping of keys starting with 'stylesheet:' with value being the stylesheet.
+ * @return array[] Mapping CSS selectors to array of properties, or mapping of keys starting with 'stylesheet:' with value being the stylesheet.
*/
public function get_styles() {
if ( ! $this->did_convert_elements ) {
@@ -209,22 +219,7 @@ public function sanitize() {
$head->appendChild( $this->amp_custom_style_element );
}
- // Gather stylesheets to print as long as they don't surpass the limit.
- $skipped = array();
- $css = '';
- $total_size = 0;
- foreach ( $this->get_stylesheets() as $key => $stylesheet ) {
- $sheet_size = strlen( $stylesheet );
- if ( $total_size + $sheet_size > $this->custom_max_size ) {
- $skipped[] = $key;
- } else {
- if ( $total_size ) {
- $css .= ' ';
- }
- $css .= $stylesheet;
- $total_size += $sheet_size;
- }
- }
+ $css = implode( '', $this->get_stylesheets() );
/*
* Let the style[amp-custom] be populated with the concatenated CSS.
@@ -236,15 +231,6 @@ public function sanitize() {
$this->amp_custom_style_element->removeChild( $this->amp_custom_style_element->firstChild );
}
$this->amp_custom_style_element->appendChild( $this->dom->createTextNode( $css ) );
-
- // @todo This would be a candidate for sanitization reporting.
- // Add comments to indicate which sheets were not included.
- foreach ( array_reverse( $skipped ) as $skip ) {
- $this->amp_custom_style_element->parentNode->insertBefore(
- $this->dom->createComment( sprintf( 'Skipped including %s stylesheet since too large.', $skip ) ),
- $this->amp_custom_style_element->nextSibling
- );
- }
}
}
@@ -303,18 +289,30 @@ public function get_validated_css_file_path( $src ) {
* @param DOMElement $element Style element.
*/
private function process_style_element( DOMElement $element ) {
- if ( 'body' === $element->parentNode->nodeName && $element->hasAttribute( 'amp-keyframes' ) ) {
+ if ( $element->hasAttribute( 'amp-keyframes' ) ) {
$validity = $this->validate_amp_keyframe( $element );
- if ( true !== $validity ) {
- $element->parentNode->removeChild( $element ); // @todo Add reporting.
+ if ( is_wp_error( $validity ) ) {
+ $this->remove_invalid_child( $element, array(
+ 'message' => $validity->get_error_message(),
+ ) );
}
return;
}
$rules = trim( $element->textContent );
- $rules = $this->remove_illegal_css( $rules );
+ $rules = $this->remove_illegal_css( $rules, $element );
+
+ // Remove if surpasses max size.
+ $length = strlen( $rules );
+ if ( $this->current_custom_size + $length > $this->custom_max_size ) {
+ $this->remove_invalid_child( $element, array(
+ 'message' => __( 'Too much CSS enqueued.', 'amp' ),
+ ) );
+ return;
+ }
$this->stylesheets[ md5( $rules ) ] = $rules;
+ $this->current_custom_size += $length;
if ( $element->hasAttribute( 'amp-custom' ) ) {
if ( ! $this->amp_custom_style_element ) {
@@ -344,22 +342,34 @@ private function process_link_element( DOMElement $element ) {
$css_file_path = $this->get_validated_css_file_path( $href );
if ( is_wp_error( $css_file_path ) ) {
- $element->parentNode->removeChild( $element ); // @todo Report removal. Show HTML comment?
+ $this->remove_invalid_child( $element, array(
+ 'message' => $css_file_path->get_error_message(),
+ ) );
return;
}
// Load the CSS from the filesystem.
- $css = "\n/* $href */\n";
- $css .= file_get_contents( $css_file_path ); // phpcs:ignore -- It's a local filesystem path not a remote request.
+ $rules = "\n/* $href */\n";
+ $rules .= file_get_contents( $css_file_path ); // phpcs:ignore -- It's a local filesystem path not a remote request.
- $css = $this->remove_illegal_css( $css );
+ $rules = $this->remove_illegal_css( $rules, $element );
$media = $element->getAttribute( 'media' );
if ( $media && 'all' !== $media ) {
- $css = sprintf( '@media %s { %s }', $media, $css );
+ $rules = sprintf( '@media %s { %s }', $media, $rules );
+ }
+
+ // Remove if surpasses max size.
+ $length = strlen( $rules );
+ if ( $this->current_custom_size + $length > $this->custom_max_size ) {
+ $this->remove_invalid_child( $element, array(
+ 'message' => __( 'Too much CSS enqueued.', 'amp' ),
+ ) );
+ return;
}
- $this->stylesheets[ $href ] = $css;
+ $this->current_custom_size += $length;
+ $this->stylesheets[ $href ] = $rules;
// Remove now that styles have been processed.
$element->parentNode->removeChild( $element );
@@ -372,12 +382,25 @@ private function process_link_element( DOMElement $element ) {
*
* @todo This needs proper CSS parser and to take an alternative approach to removing !important by extracting
* the rule into a separate style rule with a very specific selector.
- * @param string $stylesheet Stylesheet.
+ * @param string $stylesheet Stylesheet.
+ * @param DOMElement $element Element where the stylesheet came from.
* @return string Scrubbed stylesheet.
*/
- private function remove_illegal_css( $stylesheet ) {
- $stylesheet = preg_replace( '/\s*!important/', '', $stylesheet ); // Note this has to also replace inside comments to be valid.
- $stylesheet = preg_replace( '/overflow\s*:\s*(auto|scroll)\s*;?\s*/', '', $stylesheet );
+ private function remove_illegal_css( $stylesheet, $element ) {
+ $stylesheet = preg_replace( '/\s*!important/', '', $stylesheet, -1, $important_count ); // Note this has to also replace inside comments to be valid.
+ if ( $important_count > 0 && ! empty( $this->args['validation_error_callback'] ) ) {
+ call_user_func( $this->args['validation_error_callback'], array(
+ 'code' => 'css_important_removed',
+ 'node' => $element,
+ ) );
+ }
+ $stylesheet = preg_replace( '/overflow(-[xy])?\s*:\s*(auto|scroll)\s*;?\s*/', '', $stylesheet, -1, $overlow_count );
+ if ( $overlow_count > 0 && ! empty( $this->args['validation_error_callback'] ) ) {
+ call_user_func( $this->args['validation_error_callback'], array(
+ 'code' => 'css_overflow_property_removed',
+ 'node' => $element,
+ ) );
+ }
return $stylesheet;
}
@@ -391,15 +414,19 @@ private function remove_illegal_css( $stylesheet ) {
* @return true|WP_Error Validity.
*/
private function validate_amp_keyframe( $style ) {
+ if ( 'body' !== $style->parentNode->nodeName ) {
+ return new WP_Error( 'mandatory_body_child', __( 'amp-keyframes is not child of body element.', 'amp' ) );
+ }
+
if ( $this->keyframes_max_size && strlen( $style->textContent ) > $this->keyframes_max_size ) {
- return new WP_Error( 'max_bytes' );
+ return new WP_Error( 'max_bytes', __( 'amp-keyframes is too large', 'amp' ) );
}
// This logic could be in AMP_Tag_And_Attribute_Sanitizer, but since it only applies to amp-keyframes it seems unnecessary.
$next_sibling = $style->nextSibling;
while ( $next_sibling ) {
if ( $next_sibling instanceof DOMElement ) {
- return new WP_Error( 'mandatory_last_child' );
+ return new WP_Error( 'mandatory_last_child', __( 'amp-keyframes is not last element in body.', 'amp' ) );
}
$next_sibling = $next_sibling->nextSibling;
}
@@ -423,19 +450,30 @@ private function validate_amp_keyframe( $style ) {
* @param DOMElement $element Node.
*/
private function collect_inline_styles( $element ) {
- $style = $element->getAttribute( 'style' );
- if ( ! $style ) {
+ $value = $element->getAttribute( 'style' );
+ if ( ! $value ) {
return;
}
$class = $element->getAttribute( 'class' );
- $style = $this->process_style( $style );
- if ( ! empty( $style ) ) {
- $class_name = $this->generate_class_name( $style );
+ $properties = $this->process_style( $value );
+
+ if ( ! empty( $properties ) ) {
+ $class_name = $this->generate_class_name( $properties );
$new_class = trim( $class . ' ' . $class_name );
+ $selector = '.' . $class_name;
+ $length = strlen( sprintf( '%s { %s }', $selector, join( '; ', $properties ) . ';' ) );
+
+ if ( $this->current_custom_size + $length > $this->custom_max_size ) {
+ $this->remove_invalid_attribute( $element, 'style', array(
+ 'message' => __( 'Too much CSS.', 'amp' ),
+ ) );
+ return;
+ }
+
$element->setAttribute( 'class', $new_class );
- $this->styles[ '.' . $class_name ] = $style;
+ $this->styles[ $selector ] = $properties;
}
$element->removeAttribute( 'style' );
}
@@ -446,12 +484,13 @@ private function collect_inline_styles( $element ) {
* @since 0.4
*
* @param string $string Style string.
- * @return array
+ * @return array Style properties.
*/
private function process_style( $string ) {
-
- /**
+ /*
* Filter properties
+ *
+ * @todo Removed values are not reported.
*/
$string = safecss_filter_attr( esc_html( $string ) );
@@ -503,14 +542,13 @@ private function process_style( $string ) {
* @return array
*/
private function filter_style( $property, $value ) {
-
- /**
+ /*
* Remove overflow if value is `auto` or `scroll`; not allowed in AMP
*
* @todo This removal needs to be reported.
* @see https://www.ampproject.org/docs/reference/spec.html#properties
*/
- if ( preg_match( '#^overflow#i', $property ) && preg_match( '#^(auto|scroll)$#i', $value ) ) {
+ if ( preg_match( '#^overflow(-[xy])?$#i', $property ) && preg_match( '#^(auto|scroll)$#i', $value ) ) {
return array( false, false );
}
@@ -518,7 +556,7 @@ private function filter_style( $property, $value ) {
$property = 'max-width';
}
- /**
+ /*
* Remove `!important`; not allowed in AMP
*
* @todo This removal needs to be reported.
diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php
index 18f4412882e..030c2be8d3b 100644
--- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php
+++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php
@@ -1559,9 +1559,8 @@ private function remove_node( $node ) {
$node = $parent;
$parent = $parent->parentNode;
if ( $parent ) {
- $this->remove_invalid_child( $node );
+ $parent->removeChild( $node );
}
}
}
}
-
diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php
index 17d5814a6eb..2ba61273427 100644
--- a/includes/utils/class-amp-validation-utils.php
+++ b/includes/utils/class-amp-validation-utils.php
@@ -13,25 +13,176 @@
class AMP_Validation_Utils {
/**
- * Key for the markup value in the REST API endpoint.
+ * Query var that triggers validation.
*
- * @var string.
+ * @var string
*/
- const MARKUP_KEY = 'markup';
+ const VALIDATE_QUERY_VAR = 'amp_validate';
/**
- * Key for the error value in the response.
+ * Query var that enables validation debug mode, to disable removal of invalid elements/attributes.
*
- * @var string.
+ * @var string
*/
- const ERROR_KEY = 'has_error';
+ const DEBUG_QUERY_VAR = 'amp_debug';
/**
- * The nodes that the sanitizer removed.
+ * Query var for cache-busting.
*
- * @var array[][]
+ * @var string
*/
- public static $removed_nodes = array();
+ const CACHE_BUST_QUERY_VAR = 'amp_cache_bust';
+
+ /**
+ * The slug of the post type to store AMP errors.
+ *
+ * @var string
+ */
+ const POST_TYPE_SLUG = 'amp_validation_error';
+
+ /**
+ * The key in the response for the sources that have invalid output.
+ *
+ * @var string
+ */
+ const SOURCES_INVALID_OUTPUT = 'sources_with_invalid_output';
+
+ /**
+ * Validation code for an invalid element.
+ *
+ * @var string
+ */
+ const INVALID_ELEMENT_CODE = 'invalid_element';
+
+ /**
+ * Validation code for an invalid attribute.
+ *
+ * @var string
+ */
+ const INVALID_ATTRIBUTE_CODE = 'invalid_attribute';
+
+ /**
+ * Validation code for when script is enqueued (which is not allowed).
+ *
+ * @var string
+ */
+ const ENQUEUED_SCRIPT_CODE = 'enqueued_script';
+
+ /**
+ * The meta key for the AMP URL where the error occurred.
+ *
+ * @var string
+ */
+ const AMP_URL_META = 'amp_url';
+
+ /**
+ * The key for removed elements.
+ *
+ * @var string
+ */
+ const REMOVED_ELEMENTS = 'removed_elements';
+
+ /**
+ * The key for removed attributes.
+ *
+ * @var string
+ */
+ const REMOVED_ATTRIBUTES = 'removed_attributes';
+
+ /**
+ * The key for removed sources.
+ *
+ * @var string
+ */
+ const REMOVED_SOURCES = 'removed_sources';
+
+ /**
+ * The action to recheck URLs for AMP validity.
+ *
+ * @var string
+ */
+ const RECHECK_ACTION = 'amp_recheck';
+
+ /**
+ * The query arg for whether there are remaining errors after rechecking URLs.
+ *
+ * @var string
+ */
+ const REMAINING_ERRORS = 'amp_remaining_errors';
+
+ /**
+ * The query arg for the number of URLs tested.
+ *
+ * @var string
+ */
+ const URLS_TESTED = 'amp_urls_tested';
+
+ /**
+ * The nonce action for rechecking a URL.
+ *
+ * @var string
+ */
+ const NONCE_ACTION = 'amp_recheck_';
+
+ /**
+ * HTTP response header name containing JSON-serialized validation errors.
+ *
+ * @var string
+ */
+ const VALIDATION_ERRORS_RESPONSE_HEADER_NAME = 'X-AMP-Validation-Errors';
+
+ /**
+ * Transient key to store validation errors when activating a plugin.
+ *
+ * @var string
+ */
+ const PLUGIN_ACTIVATION_VALIDATION_ERRORS_TRANSIENT_KEY = 'amp_plugin_activation_validation_errors';
+
+ /**
+ * The name of the side meta box on the CPT post.php page.
+ *
+ * @var string
+ */
+ const STATUS_META_BOX = 'amp_validation_status';
+
+ /**
+ * The name of the side meta box on the CPT post.php page.
+ *
+ * @var string
+ */
+ const VALIDATION_ERRORS_META_BOX = 'amp_validation_errors';
+
+ /**
+ * The errors encountered when validating.
+ *
+ * @var array[][] {
+ * @type string $code Error code.
+ * @type string $node_name Name of removed node.
+ * @type string $parent_name Name of parent node.
+ * }
+ */
+ public static $validation_errors = array();
+
+ /**
+ * Sources that enqueue each script.
+ *
+ * @var array
+ */
+ public static $enqueued_script_sources = array();
+
+ /**
+ * Sources that enqueue each style.
+ *
+ * @var array
+ */
+ public static $enqueued_style_sources = array();
+
+ /**
+ * Post IDs for posts that have been updated which need to be re-validated.
+ *
+ * @var int[]
+ */
+ public static $posts_pending_frontend_validation = array();
/**
* Add the actions.
@@ -39,78 +190,186 @@ class AMP_Validation_Utils {
* @return void
*/
public static function init() {
- add_action( 'rest_api_init', array( __CLASS__, 'amp_rest_validation' ) );
- add_action( 'edit_form_top', array( __CLASS__, 'validate_content' ), 10, 2 );
+ if ( current_theme_supports( 'amp' ) ) {
+ add_action( 'init', array( __CLASS__, 'register_post_type' ) );
+ add_filter( 'dashboard_glance_items', array( __CLASS__, 'filter_dashboard_glance_items' ) );
+ add_action( 'rightnow_end', array( __CLASS__, 'print_dashboard_glance_styles' ) );
+ add_action( 'save_post', array( __CLASS__, 'handle_save_post_prompting_validation' ), 10, 2 );
+ }
+
+ add_action( 'edit_form_top', array( __CLASS__, 'print_edit_form_validation_status' ), 10, 2 );
+ add_action( 'all_admin_notices', array( __CLASS__, 'plugin_notice' ) );
+ add_filter( 'manage_' . self::POST_TYPE_SLUG . '_posts_columns', array( __CLASS__, 'add_post_columns' ) );
+ add_action( 'manage_posts_custom_column', array( __CLASS__, 'output_custom_column' ), 10, 2 );
+ add_filter( 'post_row_actions', array( __CLASS__, 'filter_row_actions' ), 10, 2 );
+ add_filter( 'bulk_actions-edit-' . self::POST_TYPE_SLUG, array( __CLASS__, 'add_bulk_action' ), 10, 2 );
+ add_filter( 'handle_bulk_actions-edit-' . self::POST_TYPE_SLUG, array( __CLASS__, 'handle_bulk_action' ), 10, 3 );
+ add_action( 'admin_notices', array( __CLASS__, 'remaining_error_notice' ) );
+ add_action( 'post_action_' . self::RECHECK_ACTION, array( __CLASS__, 'handle_inline_recheck' ) );
+ add_action( 'admin_menu', array( __CLASS__, 'remove_publish_meta_box' ) );
+ add_action( 'admin_menu', array( __CLASS__, 'add_admin_menu_validation_status_count' ) );
+ add_action( 'add_meta_boxes', array( __CLASS__, 'add_meta_boxes' ) );
+
+ // Actions and filters involved in validation.
+ add_action( 'activate_plugin', function() {
+ if ( ! has_action( 'shutdown', array( __CLASS__, 'validate_after_plugin_activation' ) ) ) {
+ add_action( 'shutdown', array( __CLASS__, 'validate_after_plugin_activation' ) ); // Shutdown so all plugins will have been activated.
+ }
+ } );
+ }
+
+ /**
+ * Add count of how many validation error posts there are to the admin menu.
+ */
+ public static function add_admin_menu_validation_status_count() {
+ global $submenu;
+ if ( ! isset( $submenu[ AMP_Options_Manager::OPTION_NAME ] ) ) {
+ return;
+ }
+ $count = wp_count_posts( self::POST_TYPE_SLUG );
+ if ( empty( $count->publish ) ) {
+ return;
+ }
+ foreach ( $submenu[ AMP_Options_Manager::OPTION_NAME ] as &$submenu_item ) {
+ if ( 'edit.php?post_type=' . self::POST_TYPE_SLUG === $submenu_item[2] ) {
+ $submenu_item[0] .= ' ' . esc_html( $count->publish ) . ' ';
+ break;
+ }
+ }
}
/**
- * Tracks when a sanitizer removes an node (element or attribute).
+ * Filter At a Glance items add AMP Validation Errors.
*
- * @param DOMNode $node The node which was removed.
- * @return void
+ * @param array $items At a glance items.
+ * @return array Items.
+ */
+ public static function filter_dashboard_glance_items( $items ) {
+ $counts = wp_count_posts( self::POST_TYPE_SLUG );
+ if ( ! empty( $counts->publish ) ) {
+ $items[] = sprintf(
+ '%s ',
+ esc_url( admin_url( 'edit.php?post_type=' . self::POST_TYPE_SLUG ) ),
+ esc_html( sprintf(
+ /* translators: %s is the validation error count */
+ _n( '%s AMP Validation Error', '%s AMP Validation Errors', $counts->publish, 'amp' ),
+ $counts->publish
+ ) )
+ );
+ }
+ return $items;
+ }
+
+ /**
+ * Print styles for the At a Glance widget.
+ */
+ public static function print_dashboard_glance_styles() {
+ ?>
+
+ post_type )
+ &&
+ ! wp_is_post_autosave( $post )
+ &&
+ ! wp_is_post_revision( $post )
+ );
+ if ( $should_validate_post ) {
+ self::$posts_pending_frontend_validation[] = $post_id;
+
+ // The reason for shutdown is to ensure that all postmeta changes have been saved, including whether AMP is enabled.
+ if ( ! has_action( 'shutdown', array( __CLASS__, 'validate_queued_posts_on_frontend' ) ) ) {
+ add_action( 'shutdown', array( __CLASS__, 'validate_queued_posts_on_frontend' ) );
+ }
+ }
+ }
+
+ /**
+ * Validate the posts pending frontend validation.
+ *
+ * @see AMP_Validation_Utils::handle_save_post_prompting_validation()
+ */
+ public static function validate_queued_posts_on_frontend() {
+ $posts = array_filter(
+ array_map( 'get_post', self::$posts_pending_frontend_validation ),
+ function( $post ) {
+ return $post && post_supports_amp( $post ) && 'trash' !== $post->post_status;
+ }
+ );
+
+ // @todo Only validate the first and then queue the rest in WP Cron?
+ foreach ( $posts as $post ) {
+ $url = amp_get_permalink( $post->ID );
+ if ( ! $url ) {
+ continue;
+ }
+
+ $validation_errors = self::validate_url( $url );
+ if ( is_wp_error( $validation_errors ) ) {
+ continue;
+ }
+
+ self::store_validation_errors( $validation_errors, $url );
+ }
}
/**
* Processes markup, to determine AMP validity.
*
* Passes $markup through the AMP sanitizers.
- * Also passes a 'remove_invalid_callback' to keep track of stripped attributes and nodes.
+ * Also passes a 'validation_error_callback' to keep track of stripped attributes and nodes.
*
* @param string $markup The markup to process.
- * @return void.
+ * @return string Sanitized markup.
*/
public static function process_markup( $markup ) {
- if ( ! self::has_cap() ) {
- return;
- }
-
AMP_Theme_Support::register_content_embed_handlers();
- remove_filter( 'the_content', 'wpautop' );
/** This filter is documented in wp-includes/post-template.php */
$markup = apply_filters( 'the_content', $markup );
$args = array(
- 'content_max_width' => ! empty( $content_width ) ? $content_width : AMP_Post_Template::CONTENT_MAX_WIDTH,
- 'remove_invalid_callback' => 'AMP_Validation_Utils::track_removed',
+ 'content_max_width' => ! empty( $content_width ) ? $content_width : AMP_Post_Template::CONTENT_MAX_WIDTH,
+ 'validation_error_callback' => 'AMP_Validation_Utils::add_validation_error',
);
- AMP_Content_Sanitizer::sanitize( $markup, amp_get_content_sanitizers(), $args );
- }
- /**
- * Registers the REST API endpoint for validation.
- *
- * @return void.
- */
- public static function amp_rest_validation() {
- register_rest_route( 'amp-wp/v1', '/validate', array(
- 'methods' => 'POST',
- 'callback' => array( __CLASS__, 'validate_markup' ),
- 'args' => array(
- self::MARKUP_KEY => array(
- 'validate_callback' => array( __CLASS__, 'validate_arg' ),
- ),
- ),
- 'permission_callback' => array( __CLASS__, 'has_cap' ),
- ) );
+ $results = AMP_Content_Sanitizer::sanitize( $markup, amp_get_content_sanitizers(), $args );
+ return $results[0];
}
/**
* Whether the user has the required capability.
*
* Checks for permissions before validating.
- * Also serves as the permission callback for REST requests.
*
* @return boolean $has_cap Whether the current user has the capability.
*/
@@ -119,69 +378,112 @@ public static function has_cap() {
}
/**
- * Validate the markup passed to the REST API.
+ * Add validation error.
+ *
+ * @param array $data {
+ * Data.
*
- * @param WP_REST_Request $request The REST request.
- * @return array|WP_Error.
+ * @type string $code Error code.
+ * @type DOMElement|DOMNode $node The removed node.
+ * }
*/
- public static function validate_markup( WP_REST_Request $request ) {
- $json = $request->get_json_params();
- if ( empty( $json[ self::MARKUP_KEY ] ) ) {
- return new WP_Error( 'no_markup', 'No markup passed to validator', array(
- 'status' => 404,
- ) );
+ public static function add_validation_error( array $data ) {
+ $node = null;
+
+ if ( isset( $data['node'] ) && $data['node'] instanceof DOMNode ) {
+ $node = $data['node'];
+ unset( $data['node'] );
+ $data['node_name'] = $node->nodeName;
+ $data['sources'] = self::locate_sources( $node );
+ if ( $node->parentNode ) {
+ $data['parent_name'] = $node->parentNode->nodeName;
+ }
}
- return self::get_response( $json[ self::MARKUP_KEY ] );
+ if ( $node instanceof DOMElement ) {
+ if ( ! isset( $data['code'] ) ) {
+ $data['code'] = self::INVALID_ELEMENT_CODE;
+ }
+ $data['node_attributes'] = array();
+ foreach ( $node->attributes as $attribute ) {
+ $data['node_attributes'][ $attribute->nodeName ] = $attribute->nodeValue;
+ }
+
+ $is_enqueued_link = (
+ 'link' === $node->nodeName
+ &&
+ preg_match( '/(?P.+)-css$/', (string) $node->getAttribute( 'id' ), $matches )
+ &&
+ isset( self::$enqueued_style_sources[ $matches['handle'] ] )
+ );
+ if ( $is_enqueued_link ) {
+ $data['sources'] = self::$enqueued_style_sources[ $matches['handle'] ];
+ }
+ } elseif ( $node instanceof DOMAttr ) {
+ if ( ! isset( $data['code'] ) ) {
+ $data['code'] = self::INVALID_ATTRIBUTE_CODE;
+ }
+ $data['element_attributes'] = array();
+ if ( $node->parentNode && $node->parentNode->hasAttributes() ) {
+ foreach ( $node->parentNode->attributes as $attribute ) {
+ $data['element_attributes'][ $attribute->nodeName ] = $attribute->nodeValue;
+ }
+ }
+ }
+
+ if ( ! isset( $data['code'] ) ) {
+ $data['code'] = 'unknown';
+ }
+
+ self::$validation_errors[] = $data;
}
/**
* Gets the AMP validation response.
*
- * If $markup isn't passed,
- * It will return the validation errors the sanitizers found in rendering the page.
+ * Returns the current validation errors the sanitizers found in rendering the page.
*
- * @param string $markup To validate for AMP compatibility (optional).
- * @return array $response The AMP validity of the markup.
+ * @param array $validation_errors Validation errors.
+ * @return array The AMP validity of the markup.
*/
- public static function get_response( $markup = null ) {
- $response = array();
- if ( isset( $markup ) ) {
- self::process_markup( $markup );
- $response['processed_markup'] = $markup;
- }
-
+ public static function summarize_validation_errors( $validation_errors ) {
+ $results = array();
$removed_elements = array();
$removed_attributes = array();
- foreach ( self::$removed_nodes as $removed ) {
- $node = $removed['node'];
- if ( $node instanceof DOMAttr ) {
- if ( ! isset( $removed_attributes[ $node->nodeName ] ) ) {
- $removed_attributes[ $node->nodeName ] = 1;
- } else {
- $removed_attributes[ $node->nodeName ]++;
+ $invalid_sources = array();
+ foreach ( $validation_errors as $validation_error ) {
+ $code = isset( $validation_error['code'] ) ? $validation_error['code'] : null;
+
+ if ( self::INVALID_ELEMENT_CODE === $code ) {
+ if ( ! isset( $removed_elements[ $validation_error['node_name'] ] ) ) {
+ $removed_elements[ $validation_error['node_name'] ] = 0;
}
- } elseif ( $node instanceof DOMElement ) {
- if ( ! isset( $removed_elements[ $node->nodeName ] ) ) {
- $removed_elements[ $node->nodeName ] = 1;
- } else {
- $removed_elements[ $node->nodeName ]++;
+ $removed_elements[ $validation_error['node_name'] ] += 1;
+ } elseif ( self::INVALID_ATTRIBUTE_CODE === $code ) {
+ if ( ! isset( $removed_attributes[ $validation_error['node_name'] ] ) ) {
+ $removed_attributes[ $validation_error['node_name'] ] = 0;
}
+ $removed_attributes[ $validation_error['node_name'] ] += 1;
+ }
+
+ if ( ! empty( $validation_error['sources'] ) ) {
+ $source = array_pop( $validation_error['sources'] );
+
+ $invalid_sources[ $source['type'] ][] = $source['name'];
}
}
- $response = array_merge(
+ $results = array_merge(
array(
- self::ERROR_KEY => self::was_node_removed(),
+ self::SOURCES_INVALID_OUTPUT => $invalid_sources,
),
compact(
'removed_elements',
'removed_attributes'
),
- $response );
- self::reset_removed();
+ $results );
- return $response;
+ return $results;
}
/**
@@ -191,86 +493,1203 @@ public static function get_response( $markup = null ) {
* these static values will remain.
* So reset them in case another test is needed.
*
- * @return void.
- */
- public static function reset_removed() {
- self::$removed_nodes = array();
- }
-
- /**
- * Validate the argument in the REST API request.
- *
- * It would be ideal to simply pass 'is_string' in register_rest_route().
- * But it always returned false.
- *
- * @param mixed $arg The argument to validate.
- * @return boolean $is_valid Whether the argument is valid.
+ * @return void
*/
- public static function validate_arg( $arg ) {
- return is_string( $arg );
+ public static function reset_validation_results() {
+ self::$validation_errors = array();
+ self::$enqueued_style_sources = array();
+ self::$enqueued_script_sources = array();
}
/**
* Checks the AMP validity of the post content.
*
- * If it's not valid AMP,
- * it displays an error message above the 'Classic' editor.
+ * If it's not valid AMP, it displays an error message above the 'Classic' editor.
*
* @param WP_Post $post The updated post.
- * @return void.
+ * @return void
*/
- public static function validate_content( $post ) {
+ public static function print_edit_form_validation_status( $post ) {
if ( ! post_supports_amp( $post ) || ! self::has_cap() ) {
return;
}
- AMP_Theme_Support::register_content_embed_handlers();
- /** This filter is documented in wp-includes/post-template.php */
- $filtered_content = apply_filters( 'the_content', $post->post_content, $post->ID );
- $response = self::get_response( $filtered_content );
- if ( isset( $response[ self::ERROR_KEY ] ) && ( true === $response[ self::ERROR_KEY ] ) ) {
- self::display_error( $response );
+
+ $url = null;
+ $validation_status_post = null;
+ $validation_errors = array();
+
+ // Validate post content outside frontend context.
+ if ( post_type_supports( $post->post_type, 'editor' ) ) {
+ self::process_markup( $post->post_content );
+ $validation_errors = array_merge(
+ $validation_errors,
+ self::$validation_errors
+ );
+ self::reset_validation_results();
+ }
+
+ // Incorporate frontend validation status if there is a known URL for the post.
+ if ( is_post_type_viewable( $post->post_type ) ) {
+ $url = amp_get_permalink( $post->ID );
+
+ $validation_status_post = self::get_validation_status_post( $url );
+ if ( $validation_status_post ) {
+ $data = json_decode( $validation_status_post->post_content, true );
+ if ( is_array( $data ) ) {
+ $validation_errors = array_merge( $validation_errors, $data );
+ }
+ }
+ }
+
+ if ( empty( $validation_errors ) ) {
+ return;
}
- }
- /**
- * Displays an error message on /wp-admin/post.php.
- *
- * Located at the top of the 'Classic' editor.
- * States that the content is not valid AMP.
- *
- * @param array $response The validation response, an associative array.
- * @return void.
- */
- public static function display_error( $response ) {
echo '';
- printf( '
%s
', esc_html__( 'Warning: There is content which fails AMP validation; it will be stripped when served as AMP.', 'amp' ) );
+ echo '
';
+ esc_html_e( 'Warning: There is content which fails AMP validation; it will be stripped when served as AMP.', 'amp' );
+ if ( $validation_status_post || $url ) {
+ if ( $validation_status_post ) {
+ echo sprintf(
+ ' %s ',
+ esc_url( get_edit_post_link( $validation_status_post ) ),
+ esc_html__( 'Details', 'amp' )
+ );
+ }
+ if ( $url ) {
+ if ( $validation_status_post ) {
+ echo ' | ';
+ }
+ echo sprintf(
+ ' %s ',
+ esc_url( self::get_debug_url( $url ) ),
+ esc_attr__( 'Validate URL on frontend but without invalid elements/attributes removed', 'amp' ),
+ esc_html__( 'Debug', 'amp' )
+ );
+ }
+ }
+ echo '
';
+
+ $results = self::summarize_validation_errors( array_unique( $validation_errors, SORT_REGULAR ) );
$removed_sets = array();
- if ( ! empty( $response['removed_elements'] ) && is_array( $response['removed_elements'] ) ) {
+ if ( ! empty( $results[ self::REMOVED_ELEMENTS ] ) && is_array( $results[ self::REMOVED_ELEMENTS ] ) ) {
$removed_sets[] = array(
'label' => __( 'Invalid elements:', 'amp' ),
- 'names' => array_map( 'sanitize_key', $response['removed_elements'] ),
+ 'names' => array_map( 'sanitize_key', $results[ self::REMOVED_ELEMENTS ] ),
);
}
- if ( ! empty( $response['removed_attributes'] ) && is_array( $response['removed_attributes'] ) ) {
+ if ( ! empty( $results[ self::REMOVED_ATTRIBUTES ] ) && is_array( $results[ self::REMOVED_ATTRIBUTES ] ) ) {
$removed_sets[] = array(
'label' => __( 'Invalid attributes:', 'amp' ),
- 'names' => array_map( 'sanitize_key', $response['removed_attributes'] ),
+ 'names' => array_map( 'sanitize_key', $results[ self::REMOVED_ATTRIBUTES ] ),
);
}
+ // @todo There are other kinds of errors other than REMOVED_ELEMENTS and REMOVED_ATTRIBUTES.
foreach ( $removed_sets as $removed_set ) {
printf( '
%s ', esc_html( $removed_set['label'] ) );
- $items = array();
- foreach ( $removed_set['names'] as $name => $count ) {
- if ( 1 === intval( $count ) ) {
- $items[] = sprintf( '%s
', esc_html( $name ) );
+ self::output_removed_set( $removed_set['names'] );
+ echo '
';
+ }
+
+ echo '
';
+ }
+
+ /**
+ * Get source start comment.
+ *
+ * @param string $type Extension type.
+ * @param string $name Extension name.
+ * @param array $args Args.
+ * @return string HTML Comment.
+ */
+ public static function get_source_comment_start( $type, $name, $args = array() ) {
+ $args_encoded = wp_json_encode( $args );
+ if ( '[]' === $args_encoded ) {
+ $args_encoded = '{}';
+ }
+ return sprintf( '', $type, $name, str_replace( '--', '', $args_encoded ) );
+ }
+
+ /**
+ * Get source end comment.
+ *
+ * @param string $type Extension type.
+ * @param string $name Extension name.
+ * @return string HTML Comment.
+ */
+ public static function get_source_comment_end( $type, $name ) {
+ return sprintf( '', $type, $name );
+ }
+
+ /**
+ * Parse source comment.
+ *
+ * @param DOMComment $comment Comment.
+ * @return array|null Source info or null if not a source comment.
+ */
+ public static function parse_source_comment( DOMComment $comment ) {
+ if ( ! preg_match( '#^\s*(?P/)?amp-source-stack:(?Ptheme|plugin|mu-plugin):(?P\S+)(?: (?P{.+}))?\s*$#s', $comment->nodeValue, $matches ) ) {
+ return null;
+ }
+ $source = wp_array_slice_assoc( $matches, array( 'type', 'name' ) );
+
+ $source['closing'] = ! empty( $matches['closing'] );
+ if ( isset( $matches['args'] ) ) {
+ $source['args'] = json_decode( $matches['args'], true );
+ }
+ return $source;
+ }
+
+ /**
+ * Walk back tree to find the open sources.
+ *
+ * @param DOMNode $node Node to look for.
+ * @return array[][] {
+ * The data of the removed sources (theme, plugin, or mu-plugin).
+ *
+ * @type string $name The name of the source.
+ * @type string $type The type of the source.
+ * }
+ */
+ public static function locate_sources( DOMNode $node ) {
+ $xpath = new DOMXPath( $node->ownerDocument );
+ $comments = $xpath->query( 'preceding::comment()[ contains( ., "amp-source-stack:" ) ]', $node );
+ $sources = array();
+ foreach ( $comments as $comment ) {
+ $source = self::parse_source_comment( $comment );
+ if ( $source ) {
+ if ( $source['closing'] ) {
+ array_pop( $sources );
} else {
- $items[] = sprintf( '%s
(%d)', esc_html( $name ), $count );
+ unset( $source['closing'] );
+ $sources[] = $source;
}
}
- echo implode( ', ', $items ); // WPCS: XSS OK.
- echo '
';
}
- echo '';
+ return $sources;
+ }
+
+ /**
+ * Remove source comments.
+ *
+ * @param DOMDocument $dom Document.
+ */
+ public static function remove_source_comments( $dom ) {
+ $xpath = new DOMXPath( $dom );
+ $comments = array();
+ foreach ( $xpath->query( '//comment()[ contains( ., "amp-source-stack:" ) ]' ) as $comment ) {
+ if ( self::parse_source_comment( $comment ) ) {
+ $comments[] = $comment;
+ }
+ }
+ foreach ( $comments as $comment ) {
+ $comment->parentNode->removeChild( $comment );
+ }
+ }
+
+ /**
+ * Wraps callbacks in comments to indicate to the sanitizer which extension added them.
+ *
+ * Iterates through all of the registered callbacks for actions and filters.
+ * If a callback is from a plugin and outputs markup, this wraps the markup in comments.
+ * Later, the sanitizer can identify which theme or plugin the illegal markup is from.
+ *
+ * @global array $wp_filter
+ * @return void
+ */
+ public static function callback_wrappers() {
+ global $wp_filter;
+ $pending_wrap_callbacks = array();
+ foreach ( $wp_filter as $filter_tag => $wp_hook ) {
+ foreach ( $wp_hook->callbacks as $priority => $callbacks ) {
+ foreach ( $callbacks as $callback ) {
+ $source_data = self::get_source( $callback['function'] );
+ if ( isset( $source_data ) ) {
+ $pending_wrap_callbacks[ $filter_tag ][] = array_merge(
+ $callback,
+ $source_data,
+ array(
+ 'hook' => $filter_tag,
+ ),
+ compact( 'priority' )
+ );
+ }
+ }
+ }
+ }
+
+ // Iterate over hooks to replace after iterating over all to begin with to prevent infinite loop in PHP<=5.4.
+ foreach ( $pending_wrap_callbacks as $hook => $callbacks ) {
+ foreach ( $callbacks as $callback ) {
+ remove_action( $hook, $callback['function'], $callback['priority'] );
+ $wrapped_callback = self::wrapped_callback( $callback );
+ add_action( $hook, $wrapped_callback, $callback['priority'], $callback['accepted_args'] );
+ }
+ }
+ }
+
+ /**
+ * Filters the output created by a shortcode callback.
+ *
+ * @since 0.7
+ *
+ * @param string $output Shortcode output.
+ * @param string $tag Shortcode name.
+ * @return string Output.
+ * @global array $shortcode_tags
+ */
+ public static function decorate_shortcode_source( $output, $tag ) {
+ global $shortcode_tags;
+ if ( ! isset( $shortcode_tags[ $tag ] ) ) {
+ return $output;
+ }
+ $source = self::get_source( $shortcode_tags[ $tag ] );
+ if ( empty( $source ) ) {
+ return $output;
+ }
+ $output = implode( '', array(
+ self::get_source_comment_start( $source['type'], $source['name'], array( 'shortcode' => $tag ) ),
+ $output,
+ self::get_source_comment_end( $source['type'], $source['name'] ),
+ ) );
+ return $output;
+ }
+
+ /**
+ * Gets the plugin or theme of the callback, if one exists.
+ *
+ * @param string|array $callback The callback for which to get the plugin.
+ * @return array|null {
+ * The source data.
+ *
+ * @type string $type Source type.
+ * @type string $name Source name.
+ * }
+ */
+ public static function get_source( $callback ) {
+ try {
+ if ( is_string( $callback ) && is_callable( $callback ) ) {
+ // The $callback is a function or static method.
+ $exploded_callback = explode( '::', $callback );
+ if ( count( $exploded_callback ) > 1 ) {
+ $reflection = new ReflectionClass( $exploded_callback[0] );
+ } else {
+ $reflection = new ReflectionFunction( $callback );
+ }
+ } elseif ( is_array( $callback ) && isset( $callback[0], $callback[1] ) && method_exists( $callback[0], $callback[1] ) ) {
+ // The $callback is a method.
+ $reflection = new ReflectionClass( $callback[0] );
+ } elseif ( is_object( $callback ) && ( 'Closure' === get_class( $callback ) ) ) {
+ $reflection = new ReflectionFunction( $callback );
+ }
+ } catch ( Exception $e ) {
+ return null;
+ }
+
+ $file = isset( $reflection ) ? $reflection->getFileName() : null;
+ if ( ! isset( $file ) ) {
+ return null;
+ }
+ $file = wp_normalize_path( $file );
+
+ $slug_pattern = '([^/]+)';
+ if ( preg_match( ':' . preg_quote( trailingslashit( wp_normalize_path( WP_PLUGIN_DIR ) ), ':' ) . $slug_pattern . ':s', $file, $matches ) ) {
+ $type = 'plugin';
+ $name = $matches[1];
+ } elseif ( preg_match( ':' . preg_quote( trailingslashit( wp_normalize_path( get_theme_root() ) ), ':' ) . $slug_pattern . ':s', $file, $matches ) ) {
+ $type = 'theme';
+ $name = $matches[1];
+ } elseif ( preg_match( ':' . preg_quote( trailingslashit( wp_normalize_path( WPMU_PLUGIN_DIR ) ), ':' ) . $slug_pattern . ':s', $file, $matches ) ) {
+ $type = 'mu-plugin';
+ $name = $matches[1];
+ }
+
+ if ( isset( $type, $name ) ) {
+ return compact( 'type', 'name' );
+ }
+ return null;
+ }
+
+ /**
+ * Wraps a callback in comments if it outputs markup.
+ *
+ * If the sanitizer removes markup,
+ * this indicates which plugin it was from.
+ * The call_user_func_array() logic is mainly copied from WP_Hook:apply_filters().
+ *
+ * @param array $callback {
+ * The callback data.
+ *
+ * @type callable $function
+ * @type int $accepted_args
+ * @type string $type
+ * @type string $source
+ * @type string $hook
+ * }
+ * @return closure $wrapped_callback The callback, wrapped in comments.
+ */
+ public static function wrapped_callback( $callback ) {
+ return function() use ( $callback ) {
+ global $wp_styles, $wp_scripts;
+
+ $function = $callback['function'];
+ $accepted_args = $callback['accepted_args'];
+ $args = func_get_args();
+
+ $before_styles_enqueued = array();
+ if ( isset( $wp_styles ) && isset( $wp_styles->queue ) ) {
+ $before_styles_enqueued = $wp_styles->queue;
+ }
+ $before_scripts_enqueued = array();
+ if ( isset( $wp_scripts ) && isset( $wp_scripts->queue ) ) {
+ $before_scripts_enqueued = $wp_scripts->queue;
+ }
+
+ ob_start();
+ $result = call_user_func_array( $function, array_slice( $args, 0, intval( $accepted_args ) ) );
+ $output = ob_get_clean();
+
+ // Keep track of which source enqueued the styles.
+ if ( isset( $wp_styles ) && isset( $wp_styles->queue ) ) {
+ foreach ( array_diff( $wp_styles->queue, $before_styles_enqueued ) as $handle ) {
+ $source = array_merge(
+ wp_array_slice_assoc( $callback, array( 'type', 'name' ) ),
+ array(
+ 'args' => array(
+ 'hook' => $callback['hook'],
+ ),
+ )
+ );
+
+ AMP_Validation_Utils::$enqueued_style_sources[ $handle ][] = $source;
+ }
+ }
+
+ // Keep track of which source enqueued the scripts, and immediately report validity .
+ if ( isset( $wp_scripts ) && isset( $wp_scripts->queue ) ) {
+ foreach ( array_diff( $wp_scripts->queue, $before_scripts_enqueued ) as $handle ) {
+ $source = array_merge(
+ wp_array_slice_assoc( $callback, array( 'type', 'name' ) ),
+ array(
+ 'args' => array(
+ 'hook' => $callback['hook'],
+ ),
+ )
+ );
+
+ AMP_Validation_Utils::$enqueued_script_sources[ $handle ][] = $source;
+
+ if ( isset( $wp_scripts->registered[ $handle ] ) ) {
+ self::add_validation_error( array(
+ 'code' => self::ENQUEUED_SCRIPT_CODE,
+ 'handle' => $handle,
+ 'dependency' => $wp_scripts->registered[ $handle ],
+ 'sources' => array(
+ $source,
+ ),
+ ) );
+ }
+ }
+ }
+
+ // Wrap output that contains HTML tags (as opposed to actions that trigger in HTML attributes).
+ if ( ! empty( $output ) && preg_match( '/<.+?>/s', $output ) ) {
+ echo AMP_Validation_Utils::get_source_comment_start( $callback['type'], $callback['name'], array( 'hook' => $callback['hook'] ) ); // WPCS: XSS ok.
+ echo $output; // WPCS: XSS ok.
+ echo AMP_Validation_Utils::get_source_comment_end( $callback['type'], $callback['name'] ); // WPCS: XSS ok.
+ }
+ return $result;
+ };
+ }
+
+ /**
+ * Output a removed set, each wrapped in
.
+ *
+ * @param array[][] $set {
+ * The removed elements to output.
+ *
+ * @type string $name The name of the source.
+ * @type string $count The number that were invalid.
+ * }
+ * @return void
+ */
+ protected static function output_removed_set( $set ) {
+ $items = array();
+ foreach ( $set as $name => $count ) {
+ if ( 1 === intval( $count ) ) {
+ $items[] = sprintf( '%s
', esc_html( $name ) );
+ } else {
+ $items[] = sprintf( '%s
(%d)', esc_html( $name ), $count );
+ }
+ }
+ echo implode( ', ', $items ); // WPCS: XSS OK.
+ }
+
+ /**
+ * Whether to validate the front end response.
+ *
+ * Either the user has the capability and the query var is present.
+ *
+ * @return boolean Whether to validate.
+ */
+ public static function should_validate_response() {
+ return self::has_cap() && isset( $_GET[ self::VALIDATE_QUERY_VAR ] ); // WPCS: CSRF ok.
+ }
+
+ /**
+ * Finalize validation.
+ *
+ * @param DOMDocument $dom Document.
+ * @param array $args {
+ * Args.
+ *
+ * @type bool $remove_source_comments Whether source comments should be removed. Defaults to true.
+ * @type bool $send_validation_errors_header Whether the X-AMP-Validation-Errors header should be sent. Defaults to true.
+ * @type bool $append_validation_status_comment Whether the validation errors should be appended as an HTML comment. Defaults to true.
+ * }
+ */
+ public static function finalize_validation( DOMDocument $dom, $args = array() ) {
+ $args = array_merge(
+ array(
+ 'send_validation_errors_header' => true,
+ 'remove_source_comments' => true,
+ 'append_validation_status_comment' => true,
+ ),
+ $args
+ );
+
+ if ( $args['send_validation_errors_header'] && ! headers_sent() ) {
+ self::send_validation_errors_header();
+ }
+
+ if ( $args['remove_source_comments'] ) {
+ self::remove_source_comments( $dom );
+ }
+
+ if ( $args['append_validation_status_comment'] ) {
+ $report = "\n# Validation Status\n";
+ $report .= "\n## Summary\n";
+ $report .= wp_json_encode( self::summarize_validation_errors( self::$validation_errors ), 128 /* JSON_PRETTY_PRINT */ ) . "\n";
+ $report .= "\n## Details\n";
+ $report .= wp_json_encode( self::$validation_errors, 128 /* JSON_PRETTY_PRINT */ ) . "\n";
+ $comment = $dom->createComment( $report );
+ $body = $dom->getElementsByTagName( 'body' )->item( 0 );
+ if ( $body ) {
+ $body->appendChild( $comment );
+ }
+ }
+ }
+
+ /**
+ * Adds the validation callback if front-end validation is needed.
+ *
+ * @param array $sanitizers The AMP sanitizers.
+ * @return array $sanitizers The filtered AMP sanitizers.
+ */
+ public static function add_validation_callback( $sanitizers ) {
+ foreach ( $sanitizers as $sanitizer => $args ) {
+ $sanitizers[ $sanitizer ] = array_merge(
+ $args,
+ array(
+ 'validation_error_callback' => __CLASS__ . '::add_validation_error',
+ )
+ );
+ }
+ return $sanitizers;
+ }
+
+ /**
+ * Registers the post type to store the validation errors.
+ *
+ * @return void.
+ */
+ public static function register_post_type() {
+ $post_type = register_post_type(
+ self::POST_TYPE_SLUG,
+ array(
+ 'labels' => array(
+ 'name' => _x( 'Validation Status', 'post type general name', 'amp' ),
+ 'singular_name' => __( 'validation error', 'amp' ),
+ 'not_found' => __( 'No validation errors found', 'amp' ),
+ 'not_found_in_trash' => __( 'No validation errors found in trash', 'amp' ),
+ 'search_items' => __( 'Search statuses', 'amp' ),
+ 'edit_item' => __( 'Validation Status', 'amp' ),
+ ),
+ 'supports' => false,
+ 'public' => false,
+ 'show_ui' => true,
+ 'show_in_menu' => AMP_Options_Manager::OPTION_NAME,
+ )
+ );
+
+ // Hide the add new post link.
+ $post_type->cap->create_posts = 'do_not_allow';
+ }
+
+ /**
+ * Send validation errors back in response header.
+ */
+ public static function send_validation_errors_header() {
+ header( self::VALIDATION_ERRORS_RESPONSE_HEADER_NAME . ': ' . wp_json_encode( self::$validation_errors ) );
+ }
+
+ /**
+ * Stores the validation errors.
+ *
+ * After the preprocessors run, this gets the validation response if the query var is present.
+ * It then stores the response in a custom post type.
+ * If there's already an error post for the URL, but there's no error anymore, it deletes it.
+ *
+ * @param array $validation_errors Validation errors.
+ * @param string $url URL on which the validation errors occurred.
+ * @return int|null $post_id The post ID of the custom post type used, or null.
+ * @global WP $wp
+ */
+ public static function store_validation_errors( $validation_errors, $url ) {
+ $post_for_this_url = self::get_validation_status_post( $url );
+
+ // Since there are no validation errors and there is an existing $existing_post_id, just delete the post.
+ if ( empty( $validation_errors ) ) {
+ if ( $post_for_this_url ) {
+ wp_delete_post( $post_for_this_url->ID, true );
+ }
+ return null;
+ }
+
+ $encoded_errors = wp_json_encode( $validation_errors );
+ $post_name = md5( $encoded_errors );
+
+ // If the post name is unchanged then the errors are the same and there is nothing to do.
+ if ( $post_for_this_url && $post_for_this_url->post_name === $post_name ) {
+ return $post_for_this_url->ID;
+ }
+
+ // If there already exists a post for the given validation errors, just amend the $url to the existing post.
+ $post_for_other_url = get_page_by_path( $post_name, OBJECT, self::POST_TYPE_SLUG );
+ if ( ! $post_for_other_url ) {
+ $post_for_other_url = get_page_by_path( $post_name . '__trashed', OBJECT, self::POST_TYPE_SLUG );
+ }
+ if ( $post_for_other_url ) {
+ if ( 'trash' === $post_for_other_url->post_status ) {
+ wp_untrash_post( $post_for_other_url->ID );
+ }
+ if ( ! in_array( $url, get_post_meta( $post_for_other_url->ID, self::AMP_URL_META, false ), true ) ) {
+ add_post_meta( $post_for_other_url->ID, self::AMP_URL_META, wp_slash( $url ), false );
+ }
+ return $post_for_other_url->ID;
+ }
+
+ // Otherwise, create a new validation status post, or update the existing one.
+ $post_id = wp_insert_post( wp_slash( array(
+ 'ID' => $post_for_this_url ? $post_for_this_url->ID : null,
+ 'post_type' => self::POST_TYPE_SLUG,
+ 'post_title' => $url,
+ 'post_name' => $post_name,
+ 'post_content' => $encoded_errors,
+ 'post_status' => 'publish',
+ ) ) );
+ if ( ! $post_id ) {
+ return null;
+ }
+ if ( ! in_array( $url, get_post_meta( $post_id, self::AMP_URL_META, false ), true ) ) {
+ add_post_meta( $post_id, self::AMP_URL_META, wp_slash( $url ), false );
+ }
+ return $post_id;
+ }
+
+ /**
+ * Gets the existing custom post that stores errors for the $url, if it exists.
+ *
+ * @param string $url The URL of the post.
+ * @return WP_Post|null The post of the existing custom post, or null.
+ */
+ public static function get_validation_status_post( $url ) {
+ $query = new WP_Query( array(
+ 'post_type' => self::POST_TYPE_SLUG,
+ 'post_status' => 'publish',
+ 'posts_per_page' => 1,
+ 'meta_query' => array(
+ array(
+ 'key' => self::AMP_URL_META,
+ 'value' => $url,
+ ),
+ ),
+ ) );
+ return array_shift( $query->posts );
+ }
+
+ /**
+ * Validates the latest published post.
+ *
+ * @return array|WP_Error The validation errors, or WP_Error.
+ */
+ public static function validate_after_plugin_activation() {
+ $url = amp_admin_get_preview_permalink();
+ if ( ! $url ) {
+ return new WP_Error( 'no_published_post_url_available' );
+ }
+ $validation_errors = self::validate_url( $url );
+ if ( is_array( $validation_errors ) && count( $validation_errors ) > 0 ) {
+ self::store_validation_errors( $validation_errors, $url );
+ set_transient( self::PLUGIN_ACTIVATION_VALIDATION_ERRORS_TRANSIENT_KEY, $validation_errors, 60 );
+ } else {
+ delete_transient( self::PLUGIN_ACTIVATION_VALIDATION_ERRORS_TRANSIENT_KEY );
+ }
+ return $validation_errors;
+ }
+
+ /**
+ * Validates a given URL.
+ *
+ * The validation errors will be stored in the validation status custom post type,
+ * as well as in a transient.
+ *
+ * @param string $url The URL to validate.
+ * @return array|WP_Error The validation errors, or WP_Error on error.
+ */
+ public static function validate_url( $url ) {
+ $validation_url = add_query_arg(
+ array(
+ self::VALIDATE_QUERY_VAR => 1,
+ self::CACHE_BUST_QUERY_VAR => wp_rand(),
+ ),
+ $url
+ );
+
+ $r = wp_remote_get( $validation_url, array(
+ 'cookies' => wp_unslash( $_COOKIE ),
+ 'sslverify' => false,
+ 'headers' => array(
+ 'Cache-Control' => 'no-cache',
+ ),
+ ) );
+ if ( is_wp_error( $r ) ) {
+ return $r;
+ }
+ if ( wp_remote_retrieve_response_code( $r ) >= 400 ) {
+ return new WP_Error(
+ wp_remote_retrieve_response_code( $r ),
+ wp_remote_retrieve_response_message( $r )
+ );
+ }
+ $json = wp_remote_retrieve_header( $r, self::VALIDATION_ERRORS_RESPONSE_HEADER_NAME );
+ if ( ! $json ) {
+ return new WP_Error( 'response_header_absent' );
+ }
+ $validation_errors = json_decode( $json, true );
+ if ( ! is_array( $validation_errors ) ) {
+ return new WP_Error( 'malformed_json_validation_errors' );
+ }
+
+ return $validation_errors;
+ }
+
+ /**
+ * On activating a plugin, display a notice if a plugin causes an AMP validation error.
+ *
+ * @return void
+ */
+ public static function plugin_notice() {
+ global $pagenow;
+ if ( ( 'plugins.php' === $pagenow ) && ( ! empty( $_GET['activate'] ) || ! empty( $_GET['activate-multi'] ) ) ) { // WPCS: CSRF ok.
+ $validation_errors = get_transient( self::PLUGIN_ACTIVATION_VALIDATION_ERRORS_TRANSIENT_KEY );
+ if ( empty( $validation_errors ) || ! is_array( $validation_errors ) ) {
+ return;
+ }
+ delete_transient( self::PLUGIN_ACTIVATION_VALIDATION_ERRORS_TRANSIENT_KEY );
+ $errors = self::summarize_validation_errors( $validation_errors );
+ $invalid_plugins = isset( $errors[ self::SOURCES_INVALID_OUTPUT ]['plugin'] ) ? array_unique( $errors[ self::SOURCES_INVALID_OUTPUT ]['plugin'] ) : null;
+ if ( isset( $invalid_plugins ) ) {
+ $reported_plugins = array();
+ foreach ( $invalid_plugins as $plugin ) {
+ $reported_plugins[] = sprintf( '%s
', esc_html( $plugin ) );
+ }
+
+ $more_details_link = sprintf(
+ '%s ',
+ esc_url( add_query_arg(
+ 'post_type',
+ self::POST_TYPE_SLUG,
+ admin_url( 'edit.php' )
+ ) ),
+ __( 'More details', 'amp' )
+ );
+ printf(
+ '',
+ esc_html( _n( 'Warning: The following plugin may be incompatible with AMP:', 'Warning: The following plugins may be incompatible with AMP: ', count( $invalid_plugins ), 'amp' ) ),
+ implode( ', ', $reported_plugins ),
+ $more_details_link,
+ esc_html__( 'Dismiss this notice.', 'amp' )
+ ); // WPCS: XSS ok.
+ }
+ }
+ }
+
+ /**
+ * Adds post columns to the UI for the validation errors.
+ *
+ * @param array $columns The post columns.
+ * @return array $columns The new post columns.
+ */
+ public static function add_post_columns( $columns ) {
+ $columns = array_merge(
+ $columns,
+ array(
+ 'url_count' => esc_html__( 'Count', 'amp' ),
+ self::REMOVED_ELEMENTS => esc_html__( 'Removed Elements', 'amp' ),
+ self::REMOVED_ATTRIBUTES => esc_html__( 'Removed Attributes', 'amp' ),
+ self::SOURCES_INVALID_OUTPUT => esc_html__( 'Incompatible Sources', 'amp' ),
+ )
+ );
+
+ // Move date to end.
+ if ( isset( $columns['date'] ) ) {
+ $date = $columns['date'];
+ unset( $columns['date'] );
+ $columns['date'] = $date;
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Outputs custom columns in the /wp-admin UI for the AMP validation errors.
+ *
+ * @param string $column_name The name of the column.
+ * @param int $post_id The ID of the post for the column.
+ * @return void
+ */
+ public static function output_custom_column( $column_name, $post_id ) {
+ $post = get_post( $post_id );
+ if ( self::POST_TYPE_SLUG !== $post->post_type ) {
+ return;
+ }
+ $validation_errors = json_decode( $post->post_content, true );
+ if ( ! is_array( $validation_errors ) ) {
+ return;
+ }
+ $errors = self::summarize_validation_errors( $validation_errors );
+ $urls = get_post_meta( $post_id, self::AMP_URL_META, false );
+
+ switch ( $column_name ) {
+ case 'url_count':
+ echo count( $urls );
+ break;
+ case self::REMOVED_ELEMENTS:
+ if ( ! empty( $errors[ self::REMOVED_ELEMENTS ] ) ) {
+ self::output_removed_set( $errors[ self::REMOVED_ELEMENTS ] );
+ } else {
+ esc_html_e( '--', 'amp' );
+ }
+ break;
+ case self::REMOVED_ATTRIBUTES:
+ if ( ! empty( $errors[ self::REMOVED_ATTRIBUTES ] ) ) {
+ self::output_removed_set( $errors[ self::REMOVED_ATTRIBUTES ] );
+ } else {
+ esc_html_e( '--', 'amp' );
+ }
+ break;
+ case self::SOURCES_INVALID_OUTPUT:
+ if ( isset( $errors[ self::SOURCES_INVALID_OUTPUT ] ) ) {
+ $sources = array();
+ foreach ( $errors[ self::SOURCES_INVALID_OUTPUT ] as $type => $names ) {
+ foreach ( array_unique( $names ) as $name ) {
+ $sources[] = sprintf( '%s: %s
', esc_html( $type ), esc_html( $name ) );
+ }
+ }
+ echo implode( ', ', $sources ); // WPCS: XSS ok.
+ }
+ break;
+ }
+ }
+
+ /**
+ * Adds a 'Recheck' link to the edit.php row actions.
+ *
+ * The logic to add the new action is mainly copied from WP_Posts_List_Table::handle_row_actions().
+ *
+ * @param array $actions The actions in the edit.php page.
+ * @param WP_Post $post The post for the actions.
+ * @return array $actions The filtered actions.
+ */
+ public static function filter_row_actions( $actions, $post ) {
+ if ( self::POST_TYPE_SLUG !== $post->post_type ) {
+ return $actions;
+ }
+
+ $actions['edit'] = sprintf(
+ '%s ',
+ esc_url( get_edit_post_link( $post ) ),
+ esc_html__( 'Details', 'amp' )
+ );
+ unset( $actions['inline hide-if-no-js'] );
+ $url = get_post_meta( $post->ID, self::AMP_URL_META, true );
+
+ if ( ! empty( $url ) ) {
+ $actions[ self::RECHECK_ACTION ] = self::get_recheck_link( $post, get_edit_post_link( $post->ID, 'raw' ), $url );
+ $actions[ self::DEBUG_QUERY_VAR ] = sprintf(
+ '%s ',
+ esc_url( self::get_debug_url( $url ) ),
+ esc_attr__( 'Validate URL on frontend but without invalid elements/attributes removed', 'amp' ),
+ esc_html__( 'Debug', 'amp' )
+ );
+ }
+
+ return $actions;
+ }
+
+ /**
+ * Adds a 'Recheck' bulk action to the edit.php page.
+ *
+ * @param array $actions The bulk actions in the edit.php page.
+ * @return array $actions The filtered bulk actions.
+ */
+ public static function add_bulk_action( $actions ) {
+ unset( $actions['edit'] );
+ $actions[ self::RECHECK_ACTION ] = esc_html__( 'Recheck', 'amp' );
+ return $actions;
+ }
+
+ /**
+ * Handles the 'Recheck' bulk action on the edit.php page.
+ *
+ * @param string $redirect The URL of the redirect.
+ * @param string $action The action.
+ * @param array $items The items on which to take the action.
+ * @return string $redirect The filtered URL of the redirect.
+ */
+ public static function handle_bulk_action( $redirect, $action, $items ) {
+ if ( self::RECHECK_ACTION !== $action ) {
+ return $redirect;
+ }
+ $remaining_invalid_urls = array();
+ foreach ( $items as $item ) {
+ $url = get_post_meta( $item, self::AMP_URL_META, true );
+ if ( empty( $url ) ) {
+ continue;
+ }
+
+ $validation_errors = self::validate_url( $url );
+ if ( ! is_array( $validation_errors ) ) {
+ continue;
+ }
+
+ self::store_validation_errors( $validation_errors, $url );
+ if ( ! empty( $validation_errors ) ) {
+ $remaining_invalid_urls[] = $url;
+ }
+ }
+
+ // Get the URLs that still have errors after rechecking.
+ $args = array(
+ self::URLS_TESTED => count( $items ),
+ self::REMAINING_ERRORS => empty( $remaining_invalid_urls ) ? '0' : '1',
+ );
+
+ return add_query_arg( $args, $redirect );
+ }
+
+ /**
+ * Outputs an admin notice after rechecking URL(s) on the custom post page.
+ *
+ * @return void
+ */
+ public static function remaining_error_notice() {
+ if ( ! isset( $_GET[ self::REMAINING_ERRORS ] ) || self::POST_TYPE_SLUG !== get_current_screen()->post_type ) { // WPCS: CSRF ok.
+ return;
+ }
+
+ $count_urls_tested = isset( $_GET[ self::URLS_TESTED ] ) ? intval( $_GET[ self::URLS_TESTED ] ) : 1; // WPCS: CSRF ok.
+ $errors_remain = ! empty( $_GET[ self::REMAINING_ERRORS ] ); // WPCS: CSRF ok.
+ if ( $errors_remain ) {
+ $class = 'notice-warning';
+ $message = _n( 'The rechecked URL still has validation errors.', 'The rechecked URLs still have validation errors.', $count_urls_tested, 'amp' );
+ } else {
+ $message = _n( 'The rechecked URL has no validation errors.', 'The rechecked URLs have no validation errors.', $count_urls_tested, 'amp' );
+ $class = 'updated';
+ }
+
+ printf(
+ '',
+ esc_attr( $class ),
+ esc_html( $message ),
+ esc_html__( 'Dismiss this notice.', 'amp' )
+ );
+ }
+
+ /**
+ * Handles clicking 'recheck' on the inline post actions.
+ *
+ * @param int $post_id The post ID of the recheck.
+ * @return void
+ */
+ public static function handle_inline_recheck( $post_id ) {
+ check_admin_referer( self::NONCE_ACTION . $post_id );
+ $url = get_post_meta( $post_id, self::AMP_URL_META, true );
+ if ( isset( $_GET['recheck_url'] ) ) {
+ $url = wp_validate_redirect( wp_unslash( $_GET['recheck_url'] ) );
+ }
+ $validation_errors = self::validate_url( $url );
+ $remaining_errors = true;
+ if ( is_array( $validation_errors ) ) {
+ self::store_validation_errors( $validation_errors, $url );
+ $remaining_errors = ! empty( $validation_errors );
+ }
+
+ $redirect = wp_get_referer();
+ if ( ! $redirect || empty( $validation_errors ) ) {
+ // If there are no remaining errors and the post was deleted, redirect to edit.php instead of post.php.
+ $redirect = add_query_arg(
+ 'post_type',
+ self::POST_TYPE_SLUG,
+ admin_url( 'edit.php' )
+ );
+ }
+ $args = array(
+ self::URLS_TESTED => '1',
+ self::REMAINING_ERRORS => $remaining_errors ? '1' : '0',
+ );
+ wp_safe_redirect( add_query_arg( $args, $redirect ) );
+ exit();
+ }
+
+ /**
+ * Removes the 'Publish' meta box from the CPT post.php page.
+ *
+ * @return void
+ */
+ public static function remove_publish_meta_box() {
+ remove_meta_box( 'submitdiv', self::POST_TYPE_SLUG, 'side' );
+ }
+
+ /**
+ * Adds the meta boxes to the CPT post.php page.
+ *
+ * @return void
+ */
+ public static function add_meta_boxes() {
+ add_meta_box( self::VALIDATION_ERRORS_META_BOX, __( 'Validation Errors', 'amp' ), array( __CLASS__, 'print_validation_errors_meta_box' ), self::POST_TYPE_SLUG, 'normal' );
+ add_meta_box( self::STATUS_META_BOX, __( 'Status', 'amp' ), array( __CLASS__, 'print_status_meta_box' ), self::POST_TYPE_SLUG, 'side' );
+ }
+
+ /**
+ * Outputs the markup of the side meta box in the CPT post.php page.
+ *
+ * This is partially copied from meta-boxes.php.
+ * Adds 'Published on,' and links to move to trash and recheck.
+ *
+ * @param WP_Post $post The post for which to output the box.
+ * @return void
+ */
+ public static function print_status_meta_box( $post ) {
+ $redirect_url = add_query_arg(
+ 'post',
+ $post->ID,
+ admin_url( 'post.php' )
+ );
+
+ echo '';
+ /* translators: Meta box date format */
+ $date_format = __( 'M j, Y @ H:i', 'default' );
+ echo '
';
+ /* translators: %s: The date this was published */
+ printf( __( 'Published on: %s ', 'amp' ), esc_html( date_i18n( $date_format, strtotime( $post->post_date ) ) ) ); // WPCS: XSS ok.
+ echo '
';
+ printf( '
', esc_url( get_delete_post_link( $post->ID ) ), esc_html__( 'Move to Trash', 'default' ) );
+
+ echo '
';
+ echo self::get_recheck_link( $post, $redirect_url ); // WPCS: XSS ok.
+ $url = get_post_meta( $post->ID, self::AMP_URL_META, true );
+ if ( $url ) {
+ printf(
+ ' |
%s ',
+ esc_url( self::get_debug_url( $url ) ),
+ esc_attr__( 'Validate URL on frontend but without invalid elements/attributes removed', 'amp' ),
+ esc_html__( 'Debug', 'amp' )
+ ); // WPCS: XSS ok.
+ }
+ echo '
';
+
+ echo '
';
+ }
+
+ /**
+ * Outputs the full meta box on the CPT post.php page.
+ *
+ * This displays the errors stored in the post content.
+ * These are output as stored, but using elements.
+ *
+ * @param WP_Post $post The post for which to output the box.
+ * @return void
+ */
+ public static function print_validation_errors_meta_box( $post ) {
+ $errors = json_decode( $post->post_content, true );
+ $urls = get_post_meta( $post->ID, self::AMP_URL_META, false );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ', $error['parent_name'] ) );
+ }
+ ?>
+
+ $value ) {
+ printf( ' %s="%s"', esc_html( $key ), esc_html( $value ) );
+ }
+ }
+ echo esc_html( '>…' );
+ ?>
+
+
+
+
+
+
+
+
+
+
+ $value ) {
+ if ( $key === $error['node_name'] ) {
+ echo '';
+ }
+ printf( ' %s="%s"', esc_html( $key ), esc_html( $value ) );
+ if ( $key === $error['node_name'] ) {
+ echo ' ';
+ }
+ }
+ echo esc_html( '>' );
+ ?>
+
+
+
+
+
+
+ $value ) : ?>
+
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ID, 'raw' ), $url ); // WPCS: XSS ok. ?>
+ |
+ %s',
+ esc_url( self::get_debug_url( $url ) ),
+ esc_attr__( 'Validate URL on frontend but without invalid elements/attributes removed', 'amp' ),
+ esc_html__( 'Debug', 'amp' )
+ )
+ ?>
+
+
+
+
+
+ 1,
+ self::DEBUG_QUERY_VAR => 1,
+ ),
+ $url
+ ) . '#development=1';
+ }
+
+ /**
+ * Gets the link to recheck the post for AMP validity.
+ *
+ * Appends a query var to $redirect_url.
+ * On clicking the link, it checks if errors still exist for $post.
+ *
+ * @param WP_Post $post The post storing the validation error.
+ * @param string $redirect_url The URL of the redirect.
+ * @param string $recheck_url The URL to check. Optional.
+ * @return string $link The link to recheck the post.
+ */
+ public static function get_recheck_link( $post, $redirect_url, $recheck_url = null ) {
+ return sprintf(
+ '%s ',
+ wp_nonce_url(
+ add_query_arg(
+ array(
+ 'action' => self::RECHECK_ACTION,
+ 'recheck_url' => $recheck_url,
+ ),
+ $redirect_url
+ ),
+ self::NONCE_ACTION . $post->ID
+ ),
+ esc_html__( 'Recheck the URL for AMP validity', 'amp' ),
+ esc_html__( 'Recheck', 'amp' )
+ );
}
}
diff --git a/phpcs.xml b/phpcs.xml
index 312fc87d2aa..16c6f31e59f 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -28,7 +28,7 @@
-
+
diff --git a/tests/test-amp-helper-functions.php b/tests/test-amp-helper-functions.php
index d6846c33708..f39b3350233 100644
--- a/tests/test-amp-helper-functions.php
+++ b/tests/test-amp-helper-functions.php
@@ -36,6 +36,7 @@ public function return_example_url( $url, $post_id ) {
* @covers \amp_get_permalink()
*/
public function test_amp_get_permalink_without_pretty_permalinks() {
+ remove_theme_support( 'amp' );
delete_option( 'permalink_structure' );
flush_rewrite_rules();
@@ -111,6 +112,40 @@ public function test_amp_get_permalink_with_pretty_permalinks() {
$this->assertContains( 'current_filter=amp_get_permalink', $url );
}
+ /**
+ * Test amp_get_permalink() with theme support paired mode.
+ *
+ * @covers \amp_get_permalink()
+ */
+ public function test_amp_get_permalink_with_theme_support() {
+ global $wp_rewrite;
+ add_theme_support( 'amp' );
+
+ update_option( 'permalink_structure', '/%year%/%monthnum%/%day%/%postname%/' );
+ $wp_rewrite->use_trailing_slashes = true;
+ $wp_rewrite->init();
+ $wp_rewrite->flush_rules();
+
+ $post_id = $this->factory()->post->create();
+ $this->assertEquals( get_permalink( $post_id ), amp_get_permalink( $post_id ) );
+
+ add_theme_support( 'amp', array(
+ 'template_dir' => 'amp',
+ ) );
+ }
+
+ /**
+ * Test amp_remove_endpoint.
+ *
+ * @covers \amp_remove_endpoint()
+ */
+ public function test_amp_remove_endpoint() {
+ $this->assertEquals( 'https://example.com/foo/', amp_remove_endpoint( 'https://example.com/foo/?amp' ) );
+ $this->assertEquals( 'https://example.com/foo/?#bar', amp_remove_endpoint( 'https://example.com/foo/?amp#bar' ) );
+ $this->assertEquals( 'https://example.com/foo/', amp_remove_endpoint( 'https://example.com/foo/amp/' ) );
+ $this->assertEquals( 'https://example.com/foo/?blaz', amp_remove_endpoint( 'https://example.com/foo/amp/?blaz' ) );
+ }
+
/**
* Filter calls.
*
diff --git a/tests/test-amp-style-sanitizer.php b/tests/test-amp-style-sanitizer.php
index c0190ec8811..158e7d5cfd5 100644
--- a/tests/test-amp-style-sanitizer.php
+++ b/tests/test-amp-style-sanitizer.php
@@ -173,9 +173,9 @@ public function get_link_and_style_test_data() {
),
),
'style_with_no_head' => array(
- 'Not good!',
+ 'Not good!',
array(
- 'body{color:red}',
+ 'body{color:red;}',
),
),
);
diff --git a/tests/test-class-amp-base-sanitizer.php b/tests/test-class-amp-base-sanitizer.php
index b51197854c7..f3cf1db37c7 100644
--- a/tests/test-class-amp-base-sanitizer.php
+++ b/tests/test-class-amp-base-sanitizer.php
@@ -1,9 +1,25 @@
array(
+ 'already_has_sizes' => array(
array(
'sizes' => 'blah',
),
@@ -12,12 +28,12 @@ public function get_data() {
),
),
- 'empty' => array(
+ 'empty' => array(
array(),
array(),
),
- 'no_width' => array(
+ 'no_width' => array(
array(
'height' => 100,
),
@@ -26,7 +42,7 @@ public function get_data() {
),
),
- 'no_height' => array(
+ 'no_height' => array(
array(
'width' => 200,
),
@@ -35,43 +51,43 @@ public function get_data() {
),
),
- 'enforce_sizes_no_class' => array(
+ 'enforce_sizes_no_class' => array(
array(
- 'width' => 200,
+ 'width' => 200,
'height' => 100,
),
array(
- 'width' => 200,
+ 'width' => 200,
'height' => 100,
- 'sizes' => '(min-width: 200px) 200px, 100vw',
- 'class' => 'amp-wp-enforced-sizes',
+ 'sizes' => '(min-width: 200px) 200px, 100vw',
+ 'class' => 'amp-wp-enforced-sizes',
),
),
- 'enforce_sizes_has_class' => array(
+ 'enforce_sizes_has_class' => array(
array(
- 'width' => 200,
+ 'width' => 200,
'height' => 100,
- 'class' => 'my-class',
+ 'class' => 'my-class',
),
array(
- 'width' => 200,
+ 'width' => 200,
'height' => 100,
- 'sizes' => '(min-width: 200px) 200px, 100vw',
- 'class' => 'my-class amp-wp-enforced-sizes',
+ 'sizes' => '(min-width: 200px) 200px, 100vw',
+ 'class' => 'my-class amp-wp-enforced-sizes',
),
),
- 'enforce_sizes_with_bigger_content_max_width' => array(
+ 'enforce_sizes_with_bigger_content_max_width' => array(
array(
- 'width' => 250,
+ 'width' => 250,
'height' => 100,
),
array(
- 'width' => 250,
+ 'width' => 250,
'height' => 100,
- 'sizes' => '(min-width: 250px) 250px, 100vw',
- 'class' => 'amp-wp-enforced-sizes',
+ 'sizes' => '(min-width: 250px) 250px, 100vw',
+ 'class' => 'amp-wp-enforced-sizes',
),
array(
'content_max_width' => 500,
@@ -80,14 +96,14 @@ public function get_data() {
'enforce_sizes_with_smaller_content_max_width' => array(
array(
- 'width' => 800,
+ 'width' => 800,
'height' => 350,
),
array(
- 'width' => 800,
+ 'width' => 800,
'height' => 350,
- 'sizes' => '(min-width: 675px) 675px, 100vw',
- 'class' => 'amp-wp-enforced-sizes',
+ 'sizes' => '(min-width: 675px) 675px, 100vw',
+ 'class' => 'amp-wp-enforced-sizes',
),
array(
'content_max_width' => 675,
@@ -97,31 +113,40 @@ public function get_data() {
}
/**
- * @dataProvider get_data
+ * Test AMP_Base_Sanitizer::enforce_sizes_attribute().
+ *
+ * @dataProvider get_enforce_sizes_data
+ * @param array $source_attributes Source Attrs.
+ * @param array $expected_attributes Expected Attrs.
+ * @param array $args Args.
+ * @covers AMP_Base_Sanitizer::enforce_sizes_attribute()
*/
public function test_enforce_sizes_attribute( $source_attributes, $expected_attributes, $args = array() ) {
- $sanitizer = new AMP_Test_Stub_Sanitizer( new DOMDocument, $args );
+ $sanitizer = new AMP_Test_Stub_Sanitizer( new DOMDocument(), $args );
$returned_attributes = $sanitizer->enforce_sizes_attribute( $source_attributes );
$this->assertEquals( $expected_attributes, $returned_attributes );
}
-}
-class AMP_Base_Sanitizer__Enforce_Fixed_Height__Test extends WP_UnitTestCase {
- public function get_data() {
+ /**
+ * Get enforce fixed data.
+ *
+ * @return array Data.
+ */
+ public function get_enforce_fixed_data() {
return array(
'both_dimensions_included' => array(
array(
- 'width' => 100,
+ 'width' => 100,
'height' => 100,
),
array(
- 'width' => 100,
+ 'width' => 100,
'height' => 100,
),
),
- 'both_dimensions_missing' => array(
+ 'both_dimensions_missing' => array(
array(),
array(
'height' => 400,
@@ -129,9 +154,9 @@ public function get_data() {
),
),
- 'both_dimensions_empty' => array(
+ 'both_dimensions_empty' => array(
array(
- 'width' => '',
+ 'width' => '',
'height' => '',
),
array(
@@ -140,7 +165,7 @@ public function get_data() {
),
),
- 'no_width' => array(
+ 'no_width' => array(
array(
'height' => 100,
),
@@ -150,7 +175,7 @@ public function get_data() {
),
),
- 'no_height' => array(
+ 'no_height' => array(
array(
'width' => 200,
),
@@ -163,40 +188,49 @@ public function get_data() {
}
/**
- * @dataProvider get_data
+ * Test AMP_Base_Sanitizer::enforce_fixed_height().
+ *
+ * @dataProvider get_enforce_fixed_data
+ * @param array $source_attributes Source Attrs.
+ * @param array $expected_attributes Expected Attrs.
+ * @param array $args Args.
+ * @covers AMP_Base_Sanitizer::enforce_fixed_height()
*/
public function test_enforce_fixed_height( $source_attributes, $expected_attributes, $args = array() ) {
- $sanitizer = new AMP_Test_Stub_Sanitizer( new DOMDocument, $args );
+ $sanitizer = new AMP_Test_Stub_Sanitizer( new DOMDocument(), $args );
$returned_attributes = $sanitizer->enforce_fixed_height( $source_attributes );
$this->assertEquals( $expected_attributes, $returned_attributes );
}
-}
-class AMP_Base_Sanitizer__Sanitize_Dimension__Test extends WP_UnitTestCase {
- public function get_data() {
+ /**
+ * Get sanitize_dimension data.
+ *
+ * @return array Data.
+ */
+ public function get_sanitize_dimension_data() {
return array(
- 'empty' => array(
+ 'empty' => array(
array( '', 'width' ),
'',
),
- 'empty_space' => array(
+ 'empty_space' => array(
array( ' ', 'width' ),
'',
),
- 'int' => array(
+ 'int' => array(
array( 123, 'width' ),
123,
),
- 'int_as_string' => array(
+ 'int_as_string' => array(
array( '123', 'width' ),
123,
),
- 'with_px' => array(
+ 'with_px' => array(
array( '567px', 'width' ),
567,
),
@@ -207,23 +241,23 @@ public function get_data() {
array( 'content_max_width' => 600 ),
),
- '100%_width__no_max' => array(
+ '100%_width__no_max' => array(
array( '100%', 'width' ),
'',
),
- '50%_width__with_max' => array(
+ '50%_width__with_max' => array(
array( '50%', 'width' ),
300,
array( 'content_max_width' => 600 ),
),
- '%_height' => array(
+ '%_height' => array(
array( '100%', 'height' ),
'',
),
- 'non_int' => array(
+ 'non_int' => array(
array( 'abcd', 'width' ),
'',
),
@@ -231,10 +265,16 @@ public function get_data() {
}
/**
- * @dataProvider get_data
+ * Test AMP_Base_Sanitizer::sanitize_dimension().
+ *
+ * @param array $source_params Source Attrs.
+ * @param array $expected_value Expected Attrs.
+ * @param array $args Args.
+ * @dataProvider get_sanitize_dimension_data
+ * @covers AMP_Base_Sanitizer::sanitize_dimension()
*/
- public function test_enforce_sizes_attribute( $source_params, $expected_value, $args = array() ) {
- $sanitizer = new AMP_Test_Stub_Sanitizer( new DOMDocument, $args );
+ public function test_sanitize_dimension( $source_params, $expected_value, $args = array() ) {
+ $sanitizer = new AMP_Test_Stub_Sanitizer( new DOMDocument(), $args );
list( $value, $dimension ) = $source_params;
$actual_value = $sanitizer->sanitize_dimension( $value, $dimension );
@@ -248,6 +288,7 @@ public function test_enforce_sizes_attribute( $source_params, $expected_value, $
* @covers AMP_Base_Sanitizer::remove_invalid_child()
*/
public function test_remove_child() {
+ AMP_Validation_Utils::reset_validation_results();
$parent_tag_name = 'div';
$dom_document = new DOMDocument( '1.0', 'utf-8' );
$parent = $dom_document->createElement( $parent_tag_name );
@@ -255,13 +296,15 @@ public function test_remove_child() {
$parent->appendChild( $child );
$this->assertEquals( $child, $parent->firstChild );
- $sanitizer = new AMP_Iframe_Sanitizer( $dom_document, array(
- 'remove_invalid_callback' => 'AMP_Validation_Utils::track_removed',
- ) );
+ $sanitizer = new AMP_Iframe_Sanitizer(
+ $dom_document, array(
+ 'validation_error_callback' => 'AMP_Validation_Utils::add_validation_error',
+ )
+ );
$sanitizer->remove_invalid_child( $child );
$this->assertEquals( null, $parent->firstChild );
- $this->assertCount( 1, AMP_Validation_Utils::$removed_nodes );
- $this->assertEquals( $child, AMP_Validation_Utils::$removed_nodes[0]['node'] );
+ $this->assertCount( 1, AMP_Validation_Utils::$validation_errors );
+ $this->assertEquals( $child->nodeName, AMP_Validation_Utils::$validation_errors[0]['node_name'] );
$parent->appendChild( $child );
$this->assertEquals( $child, $parent->firstChild );
@@ -269,7 +312,7 @@ public function test_remove_child() {
$this->assertEquals( null, $parent->firstChild );
$this->assertEquals( null, $child->parentNode );
- AMP_Validation_Utils::$removed_nodes = null;
+ AMP_Validation_Utils::$validation_errors = null;
}
/**
@@ -278,28 +321,31 @@ public function test_remove_child() {
* @covers AMP_Base_Sanitizer::remove_invalid_child()
*/
public function test_remove_attribute() {
- AMP_Validation_Utils::reset_removed();
+ AMP_Validation_Utils::reset_validation_results();
$video_name = 'amp-video';
$attribute = 'onload';
$dom_document = new DOMDocument( '1.0', 'utf-8' );
$video = $dom_document->createElement( $video_name );
$video->setAttribute( $attribute, 'someFunction()' );
$attr_node = $video->getAttributeNode( $attribute );
-
$args = array(
- 'remove_invalid_callback' => 'AMP_Validation_Utils::track_removed',
+ 'validation_error_callback' => 'AMP_Validation_Utils::add_validation_error',
);
$sanitizer = new AMP_Video_Sanitizer( $dom_document, $args );
$sanitizer->remove_invalid_attribute( $video, $attribute );
$this->assertEquals( null, $video->getAttribute( $attribute ) );
$this->assertEquals(
array(
- 'node' => $attr_node,
- 'parent' => $video,
+ 'code' => AMP_Validation_Utils::INVALID_ATTRIBUTE_CODE,
+ 'node_name' => $attr_node->nodeName,
+ 'parent_name' => $video->nodeName,
+ 'sources' => array(),
+ 'element_attributes' => array(
+ 'onload' => 'someFunction()',
+ ),
),
- AMP_Validation_Utils::$removed_nodes[0]
+ AMP_Validation_Utils::$validation_errors[0]
);
- AMP_Validation_Utils::reset_removed();
+ AMP_Validation_Utils::reset_validation_results();
}
-
}
diff --git a/tests/test-class-amp-options-menu.php b/tests/test-class-amp-options-menu.php
index ec5109d2612..035706e3b85 100644
--- a/tests/test-class-amp-options-menu.php
+++ b/tests/test-class-amp-options-menu.php
@@ -43,7 +43,7 @@ public function test_constants() {
*/
public function test_init() {
$this->instance->init();
- $this->assertEquals( 10, has_action( 'admin_menu', array( $this->instance, 'add_menu_items' ) ) );
+ $this->assertEquals( 9, has_action( 'admin_menu', array( $this->instance, 'add_menu_items' ) ) );
$this->assertEquals( 10, has_action( 'admin_post_amp_analytics_options', 'AMP_Options_Manager::handle_analytics_submit' ) );
}
diff --git a/tests/test-class-amp-theme-support.php b/tests/test-class-amp-theme-support.php
index 3555afca149..9f4e38078c6 100644
--- a/tests/test-class-amp-theme-support.php
+++ b/tests/test-class-amp-theme-support.php
@@ -94,8 +94,10 @@ public function test_register_widgets() {
* @covers AMP_Theme_Support::prepare_response()
*/
public function test_prepare_response() {
+ global $wp_widget_factory;
add_theme_support( 'amp' );
AMP_Theme_Support::init();
+ AMP_Theme_Support::finish_init();
$wp_widget_factory = new WP_Widget_Factory();
wp_widgets_init();
@@ -127,7 +129,7 @@ public function test_prepare_response() {
$original_html = trim( ob_get_clean() );
$removed_nodes = array();
$sanitized_html = AMP_Theme_Support::prepare_response( $original_html, array(
- 'remove_invalid_callback' => function( $removed ) use ( &$removed_nodes ) {
+ 'validation_error_callback' => function( $removed ) use ( &$removed_nodes ) {
$removed_nodes[ $removed['node']->nodeName ] = $removed['node'];
},
) );
@@ -149,7 +151,7 @@ public function test_prepare_response() {
$this->assertContains( ''; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
/**
- * A tag that the sanitizer should strip.
+ * The name of a tag that the sanitizer should strip.
*
* @var string
*/
- public $disallowed_tag = ''; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
+ public $disallowed_tag_name = 'script';
/**
- * A valid image that sanitizers should not alter.
+ * The name of an attribute that the sanitizer should strip.
*
* @var string
*/
- public $valid_amp_img = ' ';
+ public $disallowed_attribute_name = 'onload';
/**
- * The key in the response for whether it has an AMP error.
+ * A mock plugin name that outputs invalid markup.
*
* @var string
*/
- public $error_key = 'has_error';
+ public $plugin_name = 'foo-bar';
+
+ /**
+ * A valid image that sanitizers should not alter.
+ *
+ * @var string
+ */
+ public $valid_amp_img = ' ';
/**
* The name of the tag to test.
@@ -63,110 +77,125 @@ public function setUp() {
parent::setUp();
$dom_document = new DOMDocument( '1.0', 'utf-8' );
$this->node = $dom_document->createElement( self::TAG_NAME );
- AMP_Validation_Utils::reset_removed();
+ AMP_Validation_Utils::reset_validation_results();
}
/**
* Test init.
*
- * @see AMP_Validation_Utils::init()
+ * @covers AMP_Validation_Utils::init()
*/
public function test_init() {
- $this->assertEquals( 10, has_action( 'rest_api_init', 'AMP_Validation_Utils::amp_rest_validation' ) );
- $this->assertEquals( 10, has_action( 'edit_form_top', 'AMP_Validation_Utils::validate_content' ) );
+ add_theme_support( 'amp' );
+ AMP_Validation_Utils::init();
+ $this->assertEquals( 10, has_action( 'edit_form_top', self::TESTED_CLASS . '::print_edit_form_validation_status' ) );
+ $this->assertEquals( 10, has_action( 'init', self::TESTED_CLASS . '::register_post_type' ) );
+ $this->assertEquals( 10, has_action( 'all_admin_notices', self::TESTED_CLASS . '::plugin_notice' ) );
+ $this->assertEquals( 10, has_filter( 'manage_' . AMP_Validation_Utils::POST_TYPE_SLUG . '_posts_columns', self::TESTED_CLASS . '::add_post_columns' ) );
+ $this->assertEquals( 10, has_action( 'manage_posts_custom_column', self::TESTED_CLASS . '::output_custom_column' ) );
+ $this->assertEquals( 10, has_filter( 'post_row_actions', self::TESTED_CLASS . '::filter_row_actions' ) );
+ $this->assertEquals( 10, has_filter( 'bulk_actions-edit-' . AMP_Validation_Utils::POST_TYPE_SLUG, self::TESTED_CLASS . '::add_bulk_action' ) );
+ $this->assertEquals( 10, has_filter( 'handle_bulk_actions-edit-' . AMP_Validation_Utils::POST_TYPE_SLUG, self::TESTED_CLASS . '::handle_bulk_action' ) );
+ $this->assertEquals( 10, has_action( 'admin_notices', self::TESTED_CLASS . '::remaining_error_notice' ) );
+ $this->assertEquals( 10, has_action( 'admin_menu', self::TESTED_CLASS . '::remove_publish_meta_box' ) );
+ $this->assertEquals( 10, has_action( 'add_meta_boxes', self::TESTED_CLASS . '::add_meta_boxes' ) );
}
/**
- * Test track_removed.
+ * Test init.
*
- * @see AMP_Validation_Utils::track_removed()
+ * @covers AMP_Validation_Utils::add_validation_hooks()
+ */
+ public function test_add_validation_hooks() {
+ AMP_Validation_Utils::add_validation_hooks();
+ $this->assertEquals( 10, has_action( 'amp_content_sanitizers', array( self::TESTED_CLASS, 'add_validation_callback' ) ) );
+ $this->assertEquals( -1, has_action( 'do_shortcode_tag', array( self::TESTED_CLASS, 'decorate_shortcode_source' ) ) );
+ }
+
+ /**
+ * Test add_validation_error.
+ *
+ * @covers AMP_Validation_Utils::add_validation_error()
*/
public function test_track_removed() {
- $this->assertEmpty( AMP_Validation_Utils::$removed_nodes );
- AMP_Validation_Utils::track_removed( $this->node );
- AMP_Validation_Utils::track_removed( $this->node );
- $this->assertEquals( array( $this->node, $this->node ), AMP_Validation_Utils::$removed_nodes );
- AMP_Validation_Utils::reset_removed();
+ $this->assertEmpty( AMP_Validation_Utils::$validation_errors );
+ AMP_Validation_Utils::add_validation_error( array(
+ 'node' => $this->node,
+ ) );
+
+ $this->assertEquals(
+ array(
+ array(
+ 'node_name' => 'img',
+ 'sources' => array(),
+ 'code' => AMP_Validation_Utils::INVALID_ELEMENT_CODE,
+ 'node_attributes' => array(),
+ ),
+ ),
+ AMP_Validation_Utils::$validation_errors
+ );
+ AMP_Validation_Utils::reset_validation_results();
}
/**
* Test was_node_removed.
*
- * @see AMP_Validation_Utils::was_node_removed()
+ * @covers AMP_Validation_Utils::add_validation_error()
*/
public function test_was_node_removed() {
- $this->assertFalse( AMP_Validation_Utils::was_node_removed() );
- AMP_Validation_Utils::track_removed( $this->node );
- $this->assertTrue( AMP_Validation_Utils::was_node_removed() );
+ $this->assertEmpty( AMP_Validation_Utils::$validation_errors );
+ AMP_Validation_Utils::add_validation_error(
+ array(
+ 'node' => $this->node,
+ )
+ );
+ $this->assertNotEmpty( AMP_Validation_Utils::$validation_errors );
}
/**
* Test process_markup.
*
- * @see AMP_Validation_Utils::process_markup()
+ * @covers AMP_Validation_Utils::process_markup()
*/
public function test_process_markup() {
- $this->set_authorized();
+ $this->set_capability();
AMP_Validation_Utils::process_markup( $this->valid_amp_img );
- $this->assertEquals( array(), AMP_Validation_Utils::$removed_nodes );
+ $this->assertEquals( array(), AMP_Validation_Utils::$validation_errors );
- AMP_Validation_Utils::reset_removed();
+ AMP_Validation_Utils::reset_validation_results();
$video = '';
AMP_Validation_Utils::process_markup( $video );
// This isn't valid AMP, but the sanitizer should convert it to an , without stripping anything.
- $this->assertEquals( array(), AMP_Validation_Utils::$removed_nodes );
+ $this->assertEquals( array(), AMP_Validation_Utils::$validation_errors );
- AMP_Validation_Utils::reset_removed();
+ AMP_Validation_Utils::reset_validation_results();
AMP_Validation_Utils::process_markup( $this->disallowed_tag );
- $this->assertCount( 1, AMP_Validation_Utils::$removed_nodes );
- $this->assertEquals( 'script', AMP_Validation_Utils::$removed_nodes[0]['node']->nodeName );
+ $this->assertCount( 1, AMP_Validation_Utils::$validation_errors );
+ $this->assertEquals( 'script', AMP_Validation_Utils::$validation_errors[0]['node_name'] );
- AMP_Validation_Utils::reset_removed();
+ AMP_Validation_Utils::reset_validation_results();
$disallowed_style = '
';
AMP_Validation_Utils::process_markup( $disallowed_style );
- $this->assertEquals( array(), AMP_Validation_Utils::$removed_nodes );
+ $this->assertEquals( array(), AMP_Validation_Utils::$validation_errors );
- AMP_Validation_Utils::reset_removed();
+ AMP_Validation_Utils::reset_validation_results();
$invalid_video = ' ';
AMP_Validation_Utils::process_markup( $invalid_video );
- $this->assertCount( 1, AMP_Validation_Utils::$removed_nodes );
- $this->assertEquals( 'video', AMP_Validation_Utils::$removed_nodes[0]['node']->nodeName );
+ $this->assertCount( 1, AMP_Validation_Utils::$validation_errors );
+ $this->assertEquals( 'video', AMP_Validation_Utils::$validation_errors[0]['node_name'] );
+ AMP_Validation_Utils::reset_validation_results();
- AMP_Validation_Utils::reset_removed();
AMP_Validation_Utils::process_markup( 'Do it ' );
- $this->assertCount( 1, AMP_Validation_Utils::$removed_nodes );
- $this->assertEquals( 'onclick', AMP_Validation_Utils::$removed_nodes[0]['node']->nodeName );
- AMP_Validation_Utils::reset_removed();
- }
-
- /**
- * Test amp_rest_validation.
- *
- * @see AMP_Validation_Utils::amp_rest_validation()
- */
- public function test_amp_rest_validation() {
- $routes = rest_get_server()->get_routes();
- $route = $routes[ $this->expected_route ][0];
- $methods = array(
- 'POST' => true,
- );
- $args = array(
- 'markup' => array(
- 'validate_callback' => array( 'AMP_Validation_Utils', 'validate_arg' ),
- ),
- );
-
- $this->assertEquals( $args, $route['args'] );
- $this->assertEquals( array( 'AMP_Validation_Utils', 'validate_markup' ), $route['callback'] );
- $this->assertEquals( $methods, $route['methods'] );
- $this->assertEquals( array( 'AMP_Validation_Utils', 'has_cap' ), $route['permission_callback'] );
+ $this->assertCount( 1, AMP_Validation_Utils::$validation_errors );
+ $this->assertEquals( 'onclick', AMP_Validation_Utils::$validation_errors[0]['node_name'] );
+ AMP_Validation_Utils::reset_validation_results();
}
/**
* Test has_cap.
*
- * @see AMP_Validation_Utils::has_cap()
+ * @covers AMP_Validation_Utils::has_cap()
*/
public function test_has_cap() {
wp_set_current_user( $this->factory()->user->create( array(
@@ -174,104 +203,54 @@ public function test_has_cap() {
) ) );
$this->assertFalse( AMP_Validation_Utils::has_cap() );
- wp_set_current_user( $this->factory()->user->create( array(
- 'role' => 'administrator',
- ) ) );
+ $this->set_capability();
$this->assertTrue( AMP_Validation_Utils::has_cap() );
}
- /**
- * Test validate_markup.
- *
- * @see AMP_Validation_Utils::validate_markup()
- */
- public function test_validate_markup() {
- $this->set_authorized();
- $request = new WP_REST_Request( 'POST', $this->expected_route );
- $request->set_header( 'content-type', 'application/json' );
- $request->set_body( wp_json_encode( array(
- AMP_Validation_Utils::MARKUP_KEY => $this->disallowed_tag,
- ) ) );
- $response = AMP_Validation_Utils::validate_markup( $request );
- $expected_response = array(
- $this->error_key => true,
- 'removed_elements' => array(
- 'script' => 1,
- ),
- 'removed_attributes' => array(),
- 'processed_markup' => $this->disallowed_tag,
- );
- $this->assertEquals( $expected_response, $response );
-
- $request->set_body( wp_json_encode( array(
- AMP_Validation_Utils::MARKUP_KEY => $this->valid_amp_img,
- ) ) );
- $response = AMP_Validation_Utils::validate_markup( $request );
- $expected_response = array(
- $this->error_key => false,
- 'removed_elements' => array(),
- 'removed_attributes' => array(),
- 'processed_markup' => $this->valid_amp_img,
- );
- $this->assertEquals( $expected_response, $response );
- }
-
/**
* Test get_response.
*
- * @see AMP_Validation_Utils::get_response()
+ * @covers AMP_Validation_Utils::summarize_validation_errors()
*/
- public function test_get_response() {
- $this->set_authorized();
- $response = AMP_Validation_Utils::get_response( $this->disallowed_tag );
+ public function test_summarize_validation_errors() {
+ global $post;
+ $post = $this->factory()->post->create_and_get(); // WPCS: global override ok.
+ AMP_Validation_Utils::process_markup( $this->disallowed_tag );
+ $response = AMP_Validation_Utils::summarize_validation_errors( AMP_Validation_Utils::$validation_errors );
+ AMP_Validation_Utils::reset_validation_results();
$expected_response = array(
- $this->error_key => true,
- 'removed_elements' => array(
+ AMP_Validation_Utils::REMOVED_ELEMENTS => array(
'script' => 1,
),
- 'removed_attributes' => array(),
- 'processed_markup' => $this->disallowed_tag,
+ AMP_Validation_Utils::REMOVED_ATTRIBUTES => array(),
+ 'sources_with_invalid_output' => array(),
);
$this->assertEquals( $expected_response, $response );
}
/**
- * Test reset_removed
+ * Test reset_validation_results.
*
- * @see AMP_Validation_Utils::reset_removed()
+ * @covers AMP_Validation_Utils::reset_validation_results()
*/
- public function test_reset_removed() {
- AMP_Validation_Utils::$removed_nodes[] = $this->node;
- AMP_Validation_Utils::reset_removed();
- $this->assertEquals( array(), AMP_Validation_Utils::$removed_nodes );
+ public function test_reset_validation_results() {
+ AMP_Validation_Utils::add_validation_error( array(
+ 'code' => 'test',
+ ) );
+ AMP_Validation_Utils::reset_validation_results();
+ $this->assertEquals( array(), AMP_Validation_Utils::$validation_errors );
}
/**
- * Test validate_arg
+ * Test print_edit_form_validation_status
*
- * @see AMP_Validation_Utils::validate_arg()
+ * @covers AMP_Validation_Utils::print_edit_form_validation_status()
*/
- public function test_validate_arg() {
- $invalid_number = 54321;
- $invalid_array = array( 'foo', 'bar' );
- $valid_string = '
';
- $this->assertFalse( AMP_Validation_Utils::validate_arg( $invalid_number ) );
- $this->assertFalse( AMP_Validation_Utils::validate_arg( $invalid_array ) );
- $this->assertTrue( AMP_Validation_Utils::validate_arg( $valid_string ) );
- }
-
- /**
- * Test validate_content
- *
- * @see AMP_Validation_Utils::validate_content()
- */
- public function test_validate_content() {
- wp_set_current_user( $this->factory()->user->create( array(
- 'role' => 'administrator',
- ) ) );
+ public function test_print_edit_form_validation_status() {
+ $this->set_capability();
$post = $this->factory()->post->create_and_get();
ob_start();
- AMP_Validation_Utils::validate_content( $post );
+ AMP_Validation_Utils::print_edit_form_validation_status( $post );
$output = ob_get_clean();
$this->assertNotContains( 'notice notice-warning', $output );
@@ -279,76 +258,1004 @@ public function test_validate_content() {
$post->post_content = $this->disallowed_tag;
ob_start();
- AMP_Validation_Utils::validate_content( $post );
+ AMP_Validation_Utils::print_edit_form_validation_status( $post );
$output = ob_get_clean();
$this->assertContains( 'notice notice-warning', $output );
$this->assertContains( 'Warning:', $output );
$this->assertContains( 'script
', $output );
- AMP_Validation_Utils::reset_removed();
+ AMP_Validation_Utils::reset_validation_results();
$youtube = 'https://www.youtube.com/watch?v=GGS-tKTXw4Y';
$post->post_content = $youtube;
ob_start();
- AMP_Validation_Utils::validate_content( $post );
+ AMP_Validation_Utils::print_edit_form_validation_status( $post );
$output = ob_get_clean();
// The YouTube embed handler should convert the URL into a valid AMP element.
$this->assertNotContains( 'notice notice-warning', $output );
$this->assertNotContains( 'Warning:', $output );
- AMP_Validation_Utils::reset_removed();
+ AMP_Validation_Utils::reset_validation_results();
+ }
+
+ /**
+ * Test source comments.
+ *
+ * @covers AMP_Validation_Utils::locate_sources()
+ * @covers AMP_Validation_Utils::parse_source_comment()
+ * @covers AMP_Validation_Utils::get_source_comment_start()
+ * @covers AMP_Validation_Utils::get_source_comment_end()
+ * @covers AMP_Validation_Utils::remove_source_comments()
+ */
+ public function test_source_comments() {
+ $dom = AMP_DOM_Utils::get_dom_from_content( implode(
+ '',
+ array(
+ AMP_Validation_Utils::get_source_comment_start( 'plugin', 'foo', array( 'shortcode' => 'test' ) ),
+ AMP_Validation_Utils::get_source_comment_start( 'theme', 'bar', array( 'hook' => 'something' ) ),
+ 'Test ',
+ AMP_Validation_Utils::get_source_comment_end( 'theme', 'bar' ),
+ AMP_Validation_Utils::get_source_comment_end( 'plugin', 'foo' ),
+ )
+ ) );
+
+ /**
+ * Comments.
+ *
+ * @var DOMComment[] $comments
+ */
+ $comments = array();
+ $xpath = new DOMXPath( $dom );
+ foreach ( $xpath->query( '//comment()' ) as $comment ) {
+ $comments[] = $comment;
+ }
+ $this->assertCount( 4, $comments );
+
+ $sources = AMP_Validation_Utils::locate_sources( $dom->getElementById( 'test' ) );
+ $this->assertInternalType( 'array', $sources );
+ $this->assertCount( 2, $sources );
+
+ $expected = array(
+ 'type' => 'plugin',
+ 'name' => 'foo',
+ 'args' => array(
+ 'shortcode' => 'test',
+ ),
+ );
+ $this->assertEquals( $expected, $sources[0] );
+ $expected['closing'] = false;
+ $this->assertEquals( $expected, AMP_Validation_Utils::parse_source_comment( $comments[0] ) );
+ $expected['closing'] = true;
+ unset( $expected['args'] );
+ $this->assertEquals( $expected, AMP_Validation_Utils::parse_source_comment( $comments[3] ) );
+
+ $expected = array(
+ 'type' => 'theme',
+ 'name' => 'bar',
+ 'args' => array(
+ 'hook' => 'something',
+ ),
+ );
+ $this->assertEquals( $expected, $sources[1] );
+ $expected['closing'] = false;
+ $this->assertEquals( $expected, AMP_Validation_Utils::parse_source_comment( $comments[1] ) );
+ $expected['closing'] = true;
+ unset( $expected['args'] );
+ $this->assertEquals( $expected, AMP_Validation_Utils::parse_source_comment( $comments[2] ) );
+
+ AMP_Validation_Utils::remove_source_comments( $dom );
+ $this->assertEquals( 0, $xpath->query( '//comment()' )->length );
+ }
+
+ /**
+ * Test callback_wrappers
+ *
+ * @covers AMP_Validation_Utils::callback_wrappers()
+ */
+ public function test_callback_wrappers() {
+ global $post;
+ $post = $this->factory()->post->create_and_get(); // WPCS: global override ok.
+ $this->set_capability();
+ $action_non_plugin = 'foo_action';
+ $action_no_output = 'bar_action_no_output';
+ $action_function_callback = 'baz_action_function';
+ $action_no_argument = 'test_action_no_argument';
+ $action_one_argument = 'baz_action_one_argument';
+ $action_two_arguments = 'example_action_two_arguments';
+ $notice = 'Example notice';
+
+ add_action( $action_function_callback, '_amp_print_php_version_admin_notice' );
+ add_action( $action_no_argument, array( $this, 'output_div' ) );
+ add_action( $action_one_argument, array( $this, 'output_notice' ) );
+ add_action( $action_two_arguments, array( $this, 'output_message' ), 10, 2 );
+ add_action( $action_no_output, array( $this, 'get_string' ), 10, 2 );
+ add_action( $action_non_plugin, 'the_ID' );
+ add_action( $action_no_output, '__return_false' );
+
+ $this->assertEquals( 10, has_action( $action_no_argument, array( $this, 'output_div' ) ) );
+ $this->assertEquals( 10, has_action( $action_one_argument, array( $this, 'output_notice' ) ) );
+ $this->assertEquals( 10, has_action( $action_two_arguments, array( $this, 'output_message' ) ) );
+
+ $_GET[ AMP_Validation_Utils::VALIDATE_QUERY_VAR ] = 1;
+ AMP_Validation_Utils::callback_wrappers();
+ $this->assertEquals( 10, has_action( $action_non_plugin, 'the_ID' ) );
+ $this->assertNotEquals( 10, has_action( $action_no_output, array( $this, 'get_string' ) ) );
+ $this->assertNotEquals( 10, has_action( $action_no_argument, array( $this, 'output_div' ) ) );
+ $this->assertNotEquals( 10, has_action( $action_one_argument, array( $this, 'output_notice' ) ) );
+ $this->assertNotEquals( 10, has_action( $action_two_arguments, array( $this, 'output_message' ) ) );
+
+ ob_start();
+ do_action( $action_function_callback );
+ $output = ob_get_clean();
+ $this->assertContains( '', $output );
+ $this->assertContains( '
test after',
+ do_shortcode( 'before[test]after' )
+ );
+ }
+
+ /**
+ * Test get_source
+ *
+ * @covers AMP_Validation_Utils::print_edit_form_validation_status()
+ */
+ public function test_get_source() {
+ $plugin = AMP_Validation_Utils::get_source( 'amp_after_setup_theme' );
+ $this->assertContains( 'amp', $plugin['name'] );
+ $this->assertEquals( 'plugin', $plugin['type'] );
+ $the_content = AMP_Validation_Utils::get_source( 'the_content' );
+ $this->assertEquals( null, $the_content );
+ $core_function = AMP_Validation_Utils::get_source( 'the_content' );
+ $this->assertEquals( null, $core_function );
}
/**
- * Test display_error().
+ * Test wrapped_callback
*
- * @see AMP_Validation_Utils::display_error().
+ * @covers AMP_Validation_Utils::wrapped_callback()
*/
- public function test_display_error() {
- $response = array(
- AMP_Validation_Utils::ERROR_KEY => false,
+ public function test_wrapped_callback() {
+ $test_string = "
Cool! ";
+ $callback = array(
+ 'function' => function() use ( $test_string ) {
+ echo $test_string; // WPCS: XSS OK.
+ },
+ 'accepted_args' => 0,
+ 'type' => 'plugin',
+ 'name' => 'amp',
+ 'hook' => 'bar',
+ );
+
+ $wrapped_callback = AMP_Validation_Utils::wrapped_callback( $callback );
+ $this->assertTrue( $wrapped_callback instanceof Closure );
+ ob_start();
+ call_user_func( $wrapped_callback );
+ $output = ob_get_clean();
+
+ $this->assertEquals( 'Closure', get_class( $wrapped_callback ) );
+ $this->assertContains( $test_string, $output );
+ $this->assertContains( '' . $this->disallowed_tag . '' );
+
+ $this->assertCount( 1, AMP_Validation_Utils::$validation_errors );
+ $this->assertEquals( 'script', AMP_Validation_Utils::$validation_errors[0]['node_name'] );
+ $this->assertEquals(
+ array(
+ 'type' => 'plugin',
+ 'name' => 'foo',
+ ),
+ AMP_Validation_Utils::$validation_errors[0]['sources'][0]
+ );
+
+ $url = home_url( '/' );
+ $post_id = AMP_Validation_Utils::store_validation_errors( AMP_Validation_Utils::$validation_errors, $url );
+ $this->assertNotEmpty( $post_id );
+ $custom_post = get_post( $post_id );
+ $validation = AMP_Validation_Utils::summarize_validation_errors( json_decode( $custom_post->post_content, true ) );
+ $expected_removed_elements = array(
+ 'script' => 1,
+ );
+ AMP_Validation_Utils::reset_validation_results();
+
+ // This should create a new post for the errors.
+ $this->assertEquals( AMP_Validation_Utils::POST_TYPE_SLUG, $custom_post->post_type );
+ $this->assertEquals( $expected_removed_elements, $validation[ AMP_Validation_Utils::REMOVED_ELEMENTS ] );
+ $this->assertEquals( array(), $validation[ AMP_Validation_Utils::REMOVED_ATTRIBUTES ] );
+ $this->assertEquals( array( 'foo' ), $validation[ AMP_Validation_Utils::SOURCES_INVALID_OUTPUT ]['plugin'] );
+ $meta = get_post_meta( $post_id, AMP_Validation_Utils::AMP_URL_META, true );
+ $this->assertEquals( $url, $meta );
+
+ AMP_Validation_Utils::reset_validation_results();
+ $url = home_url( '/?baz' );
+ AMP_Validation_Utils::process_markup( '' . $this->disallowed_tag . '' );
+ $custom_post_id = AMP_Validation_Utils::store_validation_errors( AMP_Validation_Utils::$validation_errors, $url );
+ AMP_Validation_Utils::reset_validation_results();
+ $meta = get_post_meta( $post_id, AMP_Validation_Utils::AMP_URL_META, false );
+ // A post exists for these errors, so the URL should be stored in the 'additional URLs' meta data.
+ $this->assertEquals( $post_id, $custom_post_id );
+ $this->assertContains( $url, $meta );
+
+ $url = home_url( '/?foo-bar' );
+ AMP_Validation_Utils::process_markup( '' . $this->disallowed_tag . '' );
+ $custom_post_id = AMP_Validation_Utils::store_validation_errors( AMP_Validation_Utils::$validation_errors, $url );
+ AMP_Validation_Utils::reset_validation_results();
+ $meta = get_post_meta( $post_id, AMP_Validation_Utils::AMP_URL_META, false );
+
+ // The URL should again be stored in the 'additional URLs' meta data.
+ $this->assertEquals( $post_id, $custom_post_id );
+ $this->assertContains( $url, $meta );
+
+ AMP_Validation_Utils::reset_validation_results();
+ AMP_Validation_Utils::process_markup( '
' );
+ $custom_post_id = AMP_Validation_Utils::store_validation_errors( AMP_Validation_Utils::$validation_errors, $url );
+ AMP_Validation_Utils::reset_validation_results();
+ $error_post = get_post( $custom_post_id );
+ $validation = AMP_Validation_Utils::summarize_validation_errors( json_decode( $error_post->post_content, true ) );
+ $expected_removed_elements = array(
+ 'nonexistent' => 1,
+ );
+
+ // A post already exists for this URL, so it should be updated.
+ $this->assertEquals( $expected_removed_elements, $validation[ AMP_Validation_Utils::REMOVED_ELEMENTS ] );
+ $this->assertEquals( array( 'foo' ), $validation[ AMP_Validation_Utils::SOURCES_INVALID_OUTPUT ]['plugin'] );
+ $this->assertContains( $url, get_post_meta( $custom_post_id, AMP_Validation_Utils::AMP_URL_META, false ) );
+
+ AMP_Validation_Utils::reset_validation_results();
+ AMP_Validation_Utils::process_markup( $this->valid_amp_img );
+
+ // There are no errors, so the existing error post should be deleted.
+ $custom_post_id = AMP_Validation_Utils::store_validation_errors( AMP_Validation_Utils::$validation_errors, $url );
+ AMP_Validation_Utils::reset_validation_results();
+
+ $this->assertNull( $custom_post_id );
+ remove_theme_support( 'amp' );
+ }
+
+ /**
+ * Test for store_validation_errors() when existing post is trashed.
+ *
+ * @covers AMP_Validation_Utils::store_validation_errors()
+ */
+ public function test_store_validation_errors_untrashing() {
+ $validation_errors = $this->get_mock_errors();
+
+ $first_post_id = AMP_Validation_Utils::store_validation_errors( $validation_errors, home_url( '/foo/' ) );
+ $this->assertInternalType( 'int', $first_post_id );
+
+ $post_name = get_post( $first_post_id )->post_name;
+ wp_trash_post( $first_post_id );
+ $this->assertEquals( $post_name . '__trashed', get_post( $first_post_id )->post_name );
+
+ $next_post_id = AMP_Validation_Utils::store_validation_errors( $validation_errors, home_url( '/bar/' ) );
+ $this->assertInternalType( 'int', $next_post_id );
+ $this->assertEquals( $post_name, get_post( $next_post_id )->post_name );
+ $this->assertEquals( $next_post_id, $first_post_id );
+
+ $this->assertEqualSets(
+ array(
+ home_url( '/foo/' ),
+ home_url( '/bar/' ),
+ ),
+ get_post_meta( $next_post_id, AMP_Validation_Utils::AMP_URL_META, false )
+ );
+ }
+
+ /**
+ * Test for get_validation_status_post().
+ *
+ * @covers AMP_Validation_Utils::get_validation_status_post()
+ */
+ public function test_get_validation_status_post() {
+ global $post;
+ $post = $this->factory()->post->create_and_get(); // WPCS: global override ok.
+ $custom_post_id = $this->factory()->post->create( array(
+ 'post_type' => AMP_Validation_Utils::POST_TYPE_SLUG,
+ ) );
+
+ $url = get_permalink( $custom_post_id );
+ $this->assertEquals( null, AMP_Validation_Utils::get_validation_status_post( $url ) );
+
+ update_post_meta( $custom_post_id, AMP_Validation_Utils::AMP_URL_META, $url );
+ $this->assertEquals( $custom_post_id, AMP_Validation_Utils::get_validation_status_post( $url )->ID );
+ }
+
+ /**
+ * Test for validate_after_plugin_activation().
+ *
+ * @covers AMP_Validation_Utils::validate_after_plugin_activation()
+ */
+ public function test_validate_after_plugin_activation() {
+ add_filter( 'amp_pre_get_permalink', '__return_empty_string' );
+ $r = AMP_Validation_Utils::validate_after_plugin_activation();
+ $this->assertInstanceOf( 'WP_Error', $r );
+ $this->assertEquals( 'no_published_post_url_available', $r->get_error_code() );
+ remove_filter( 'amp_pre_get_permalink', '__return_empty_string' );
+
+ $validation_errors = array(
+ array(
+ 'code' => 'example',
),
- 'removed_attributes' => array(
- $removed_attribute => 1,
+ );
+
+ $this->factory()->post->create();
+ $filter = function() use ( $validation_errors ) {
+ return array(
+ 'body' => '',
+ 'headers' => array(
+ AMP_Validation_Utils::VALIDATION_ERRORS_RESPONSE_HEADER_NAME => wp_json_encode( $validation_errors ),
+ ),
+ );
+ };
+ add_filter( 'pre_http_request', $filter, 10, 3 );
+ $r = AMP_Validation_Utils::validate_after_plugin_activation();
+ remove_filter( 'pre_http_request', $filter );
+ $this->assertEquals( $validation_errors, $r );
+ $this->assertEquals( $validation_errors, get_transient( AMP_Validation_Utils::PLUGIN_ACTIVATION_VALIDATION_ERRORS_TRANSIENT_KEY ) );
+ }
+
+ /**
+ * Test for validate_url().
+ *
+ * @covers AMP_Validation_Utils::validate_url()
+ */
+ public function test_validate_url() {
+ $validation_errors = array(
+ array(
+ 'code' => 'example',
),
);
+
+ // Test headers absent.
+ $this->factory()->post->create();
+ $filter = function() use ( $validation_errors ) {
+ return array(
+ 'body' => '',
+ 'headers' => array(),
+ );
+ };
+ add_filter( 'pre_http_request', $filter );
+ $r = AMP_Validation_Utils::validate_url( home_url( '/' ) );
+ $this->assertInstanceOf( 'WP_Error', $r );
+ $this->assertEquals( 'response_header_absent', $r->get_error_code() );
+ remove_filter( 'pre_http_request', $filter );
+
+ // Test success.
+ $that = $this;
+ $validated_url = home_url( '/foo/' );
+ $filter = function( $pre, $r, $url ) use ( $validation_errors, $validated_url, $that ) {
+ unset( $pre, $r );
+ $that->assertStringStartsWith(
+ add_query_arg(
+ AMP_Validation_Utils::VALIDATE_QUERY_VAR,
+ 1,
+ $validated_url
+ ),
+ $url
+ );
+ return array(
+ 'body' => '',
+ 'headers' => array(
+ AMP_Validation_Utils::VALIDATION_ERRORS_RESPONSE_HEADER_NAME => wp_json_encode( $validation_errors ),
+ ),
+ );
+ };
+ add_filter( 'pre_http_request', $filter, 10, 3 );
+ $r = AMP_Validation_Utils::validate_url( $validated_url );
+ $this->assertEquals( $validation_errors, $r );
+ remove_filter( 'pre_http_request', $filter );
+ }
+
+ /**
+ * Test for plugin_notice()
+ *
+ * @covers AMP_Validation_Utils::plugin_notice()
+ */
+ public function test_plugin_notice() {
+ global $pagenow;
ob_start();
- AMP_Validation_Utils::display_error( $response );
+ AMP_Validation_Utils::plugin_notice();
$output = ob_get_clean();
- $this->assertContains( 'notice notice-warning', $output );
- $this->assertContains( 'Warning:', $output );
- $this->assertContains( $removed_element, $output );
- $this->assertContains( $removed_attribute, $output );
+ $this->assertEmpty( $output );
+ $pagenow = 'plugins.php'; // WPCS: global override ok.
+ $_GET['activate'] = 'true';
+
+ set_transient( AMP_Validation_Utils::PLUGIN_ACTIVATION_VALIDATION_ERRORS_TRANSIENT_KEY, array(
+ array(
+ 'code' => 'example',
+ 'sources' => array(
+ array(
+ 'type' => 'plugin',
+ 'name' => 'foo-bar',
+ ),
+ ),
+ ),
+ ) );
+ ob_start();
+ AMP_Validation_Utils::plugin_notice();
+ $output = ob_get_clean();
+ $this->assertContains( 'Warning: The following plugin may be incompatible with AMP', $output );
+ $this->assertContains( $this->plugin_name, $output );
+ $this->assertContains( 'More details', $output );
+ $this->assertContains( admin_url( 'edit.php' ), $output );
}
/**
- * Add a nonce to the $_REQUEST, so that is_authorized() returns true.
+ * Test for add_post_columns()
+ *
+ * @covers AMP_Validation_Utils::add_post_columns()
+ */
+ public function test_add_post_columns() {
+ $initial_columns = array(
+ 'cb' => '
',
+ );
+ $this->assertEquals(
+ array_merge(
+ $initial_columns,
+ array(
+ 'url_count' => 'Count',
+ AMP_Validation_Utils::REMOVED_ELEMENTS => 'Removed Elements',
+ AMP_Validation_Utils::REMOVED_ATTRIBUTES => 'Removed Attributes',
+ AMP_Validation_Utils::SOURCES_INVALID_OUTPUT => 'Incompatible Sources',
+ )
+ ),
+ AMP_Validation_Utils::add_post_columns( $initial_columns )
+ );
+ }
+
+ /**
+ * Test for output_custom_column()
+ *
+ * @dataProvider get_custom_columns
+ * @covers AMP_Validation_Utils::output_custom_column()
+ * @param string $column_name The name of the column.
+ * @param string $expected_value The value that is expected to be present in the column markup.
+ */
+ public function test_output_custom_column( $column_name, $expected_value ) {
+ ob_start();
+ AMP_Validation_Utils::output_custom_column( $column_name, $this->create_custom_post() );
+ $this->assertContains( $expected_value, ob_get_clean() );
+ }
+
+ /**
+ * Gets the test data for test_output_custom_column().
+ *
+ * @return array $columns
+ */
+ public function get_custom_columns() {
+ return array(
+ 'url_count' => array(
+ 'url_count',
+ '1',
+ ),
+ 'invalid_element' => array(
+ AMP_Validation_Utils::REMOVED_ELEMENTS,
+ $this->disallowed_tag_name,
+ ),
+ 'removed_attributes' => array(
+ AMP_Validation_Utils::REMOVED_ATTRIBUTES,
+ $this->disallowed_attribute_name,
+ ),
+ 'sources_invalid_input' => array(
+ AMP_Validation_Utils::SOURCES_INVALID_OUTPUT,
+ $this->plugin_name,
+ ),
+ );
+ }
+
+ /**
+ * Test for filter_row_actions()
+ *
+ * @covers AMP_Validation_Utils::filter_row_actions()
+ */
+ public function test_filter_row_actions() {
+ $this->set_capability();
+
+ $initial_actions = array(
+ 'trash' => '
Trash ',
+ );
+ $post = $this->factory()->post->create_and_get();
+ $this->assertEquals( $initial_actions, AMP_Validation_Utils::filter_row_actions( $initial_actions, $post ) );
+
+ $custom_post_id = $this->create_custom_post();
+ $actions = AMP_Validation_Utils::filter_row_actions( $initial_actions, get_post( $custom_post_id ) );
+ $url = get_post_meta( $custom_post_id, AMP_Validation_Utils::AMP_URL_META, true );
+ $this->assertContains( $url, $actions[ AMP_Validation_Utils::RECHECK_ACTION ] );
+ $this->assertEquals( $initial_actions['trash'], $actions['trash'] );
+ }
+
+ /**
+ * Test for add_bulk_action()
+ *
+ * @covers AMP_Validation_Utils::add_bulk_action()
+ */
+ public function test_add_bulk_action() {
+ $initial_action = array(
+ 'edit' => 'Edit',
+ );
+ $actions = AMP_Validation_Utils::add_bulk_action( $initial_action );
+ $this->assertFalse( isset( $action['edit'] ) );
+ $this->assertEquals( 'Recheck', $actions[ AMP_Validation_Utils::RECHECK_ACTION ] );
+ }
+
+ /**
+ * Test for handle_bulk_action()
+ *
+ * @covers AMP_Validation_Utils::handle_bulk_action()
+ */
+ public function test_handle_bulk_action() {
+ $initial_redirect = admin_url( 'plugins.php' );
+ $items = array( $this->create_custom_post() );
+ $urls_tested = '1';
+ $_GET[ AMP_Validation_Utils::URLS_TESTED ] = $urls_tested;
+
+ // The action isn't correct, so the callback should return the URL unchanged.
+ $this->assertEquals( $initial_redirect, AMP_Validation_Utils::handle_bulk_action( $initial_redirect, 'trash', $items ) );
+
+ $that = $this;
+ $filter = function() use ( $that ) {
+ return array(
+ 'body' => '',
+ 'headers' => array(
+ AMP_Validation_Utils::VALIDATION_ERRORS_RESPONSE_HEADER_NAME => wp_json_encode( $that->get_mock_errors() ),
+ ),
+ );
+ };
+ add_filter( 'pre_http_request', $filter, 10, 3 );
+ $this->assertEquals(
+ add_query_arg(
+ array(
+ AMP_Validation_Utils::URLS_TESTED => $urls_tested,
+ AMP_Validation_Utils::REMAINING_ERRORS => count( $items ),
+ ),
+ $initial_redirect
+ ),
+ AMP_Validation_Utils::handle_bulk_action( $initial_redirect, AMP_Validation_Utils::RECHECK_ACTION, $items )
+ );
+ remove_filter( 'pre_http_request', $filter, 10, 3 );
+ }
+
+ /**
+ * Test for remaining_error_notice()
+ *
+ * @covers AMP_Validation_Utils::remaining_error_notice()
+ */
+ public function test_remaining_error_notice() {
+ ob_start();
+ AMP_Validation_Utils::remaining_error_notice();
+ $this->assertEmpty( ob_get_clean() );
+
+ $_GET['post_type'] = 'post';
+ ob_start();
+ AMP_Validation_Utils::remaining_error_notice();
+ $this->assertEmpty( ob_get_clean() );
+
+ set_current_screen( 'edit.php' );
+ get_current_screen()->post_type = AMP_Validation_Utils::POST_TYPE_SLUG;
+
+ $_GET[ AMP_Validation_Utils::REMAINING_ERRORS ] = '1';
+ $_GET[ AMP_Validation_Utils::URLS_TESTED ] = '1';
+ ob_start();
+ AMP_Validation_Utils::remaining_error_notice();
+ $this->assertContains( 'The rechecked URL still has validation errors', ob_get_clean() );
+
+ $_GET[ AMP_Validation_Utils::URLS_TESTED ] = '2';
+ ob_start();
+ AMP_Validation_Utils::remaining_error_notice();
+ $this->assertContains( 'The rechecked URLs still have validation errors', ob_get_clean() );
+
+ $_GET[ AMP_Validation_Utils::REMAINING_ERRORS ] = '0';
+ ob_start();
+ AMP_Validation_Utils::remaining_error_notice();
+ $this->assertContains( 'The rechecked URLs have no validation error', ob_get_clean() );
+
+ $_GET[ AMP_Validation_Utils::URLS_TESTED ] = '1';
+ ob_start();
+ AMP_Validation_Utils::remaining_error_notice();
+ $this->assertContains( 'The rechecked URL has no validation error', ob_get_clean() );
+ }
+
+ /**
+ * Test for handle_inline_recheck()
*
- * @return void.
+ * @covers AMP_Validation_Utils::handle_inline_recheck()
*/
- public function set_authorized() {
- global $_REQUEST, $post; // WPCS: CSRF ok.
- if ( ! AMP_Validation_Utils::has_cap() ) {
- wp_set_current_user( $this->factory()->user->create( array(
- 'role' => 'administrator',
- ) ) );
+ public function test_handle_inline_recheck() {
+ $post_id = $this->create_custom_post();
+ $_REQUEST['_wpnonce'] = wp_create_nonce( AMP_Validation_Utils::NONCE_ACTION . $post_id );
+ wp_set_current_user( $this->factory()->user->create( array(
+ 'role' => 'administrator',
+ ) ) );
+
+ try {
+ AMP_Validation_Utils::handle_inline_recheck( $post_id );
+ } catch ( WPDieException $e ) {
+ $exception = $e;
}
- $post_id = $this->factory()->post->create();
- $post = get_post( $post_id ); // WPCS: global override ok.
- $_REQUEST['_wpnonce'] = wp_create_nonce( 'update-post_' . $post_id ); // WPCS: global override ok.
+
+ // This calls wp_redirect(), which throws an exception.
+ $this->assertTrue( isset( $exception ) );
+ }
+
+ /**
+ * Test for remove_publish_meta_box()
+ *
+ * @covers AMP_Validation_Utils::remove_publish_meta_box()
+ */
+ public function test_remove_publish_meta_box() {
+ global $wp_meta_boxes;
+ AMP_Validation_Utils::remove_publish_meta_box();
+ $contexts = $wp_meta_boxes[ AMP_Validation_Utils::POST_TYPE_SLUG ]['side'];
+ foreach ( $contexts as $context ) {
+ $this->assertFalse( $context['submitdiv'] );
+ }
+ }
+
+ /**
+ * Test for add_meta_boxes()
+ *
+ * @covers AMP_Validation_Utils::add_meta_boxes()
+ */
+ public function test_add_meta_boxes() {
+ global $wp_meta_boxes;
+ AMP_Validation_Utils::add_meta_boxes();
+ $side_meta_box = $wp_meta_boxes[ AMP_Validation_Utils::POST_TYPE_SLUG ]['side']['default'][ AMP_Validation_Utils::STATUS_META_BOX ];
+ $this->assertEquals( AMP_Validation_Utils::STATUS_META_BOX, $side_meta_box['id'] );
+ $this->assertEquals( 'Status', $side_meta_box['title'] );
+ $this->assertEquals(
+ array(
+ self::TESTED_CLASS,
+ 'print_status_meta_box',
+ ),
+ $side_meta_box['callback']
+ );
+
+ $full_meta_box = $wp_meta_boxes[ AMP_Validation_Utils::POST_TYPE_SLUG ]['normal']['default'][ AMP_Validation_Utils::VALIDATION_ERRORS_META_BOX ];
+ $this->assertEquals( AMP_Validation_Utils::VALIDATION_ERRORS_META_BOX, $full_meta_box['id'] );
+ $this->assertEquals( 'Validation Errors', $full_meta_box['title'] );
+ $this->assertEquals(
+ array(
+ self::TESTED_CLASS,
+ 'print_validation_errors_meta_box',
+ ),
+ $full_meta_box['callback']
+ );
+ }
+
+ /**
+ * Test for print_status_meta_box()
+ *
+ * @covers AMP_Validation_Utils::print_status_meta_box()
+ */
+ public function test_print_status_meta_box() {
+ $this->set_capability();
+ $post_storing_error = get_post( $this->create_custom_post() );
+ $url = get_post_meta( $post_storing_error->ID, AMP_Validation_Utils::AMP_URL_META, true );
+ $post_with_error = AMP_Validation_Utils::get_validation_status_post( $url );
+ ob_start();
+ AMP_Validation_Utils::print_status_meta_box( $post_storing_error );
+ $output = ob_get_clean();
+
+ $this->assertContains( date_i18n( 'M j, Y @ H:i', strtotime( $post_with_error->post_date ) ), $output );
+ $this->assertContains( 'Published on:', $output );
+ $this->assertContains( 'Move to Trash', $output );
+ $this->assertContains( esc_url( get_delete_post_link( $post_storing_error->ID ) ), $output );
+ $this->assertContains( 'misc-pub-section', $output );
+ $this->assertContains(
+ AMP_Validation_Utils::get_recheck_link(
+ $post_with_error,
+ add_query_arg(
+ 'post',
+ $post_with_error->ID,
+ admin_url( 'post.php' )
+ )
+ ),
+ $output
+ );
+ }
+
+ /**
+ * Test for print_status_meta_box()
+ *
+ * @covers AMP_Validation_Utils::print_status_meta_box()
+ */
+ public function test_print_validation_errors_meta_box() {
+ $this->set_capability();
+ $post_storing_error = get_post( $this->create_custom_post() );
+ $first_url = get_post_meta( $post_storing_error->ID, AMP_Validation_Utils::AMP_URL_META, true );
+ $second_url_same_errors = get_permalink( $this->factory()->post->create() );
+ AMP_Validation_Utils::store_validation_errors( $this->get_mock_errors(), $second_url_same_errors );
+ ob_start();
+ AMP_Validation_Utils::print_validation_errors_meta_box( $post_storing_error );
+ $output = ob_get_clean();
+
+ $this->assertContains( '
assertContains( $this->disallowed_tag_name, $output );
+ $this->assertContains( $this->disallowed_attribute_name, $output );
+ $this->assertContains( 'URLs', $output );
+ $this->assertContains( $first_url, $output );
+ $this->assertContains( $second_url_same_errors, $output );
+ AMP_Validation_Utils::reset_validation_results();
+ }
+
+ /**
+ * Test for get_debug_url()
+ *
+ * @covers AMP_Validation_Utils::get_debug_url()
+ */
+ public function test_get_debug_url() {
+ $this->assertContains( AMP_Validation_Utils::VALIDATE_QUERY_VAR . '=1', AMP_Validation_Utils::get_debug_url( 'https://example.com/foo/' ) );
+ $this->assertContains( AMP_Validation_Utils::DEBUG_QUERY_VAR . '=1', AMP_Validation_Utils::get_debug_url( 'https://example.com/foo/' ) );
+ $this->assertStringEndsWith( '#development=1', AMP_Validation_Utils::get_debug_url( 'https://example.com/foo/' ) );
+ }
+
+ /**
+ * Test for get_recheck_link()
+ *
+ * @covers AMP_Validation_Utils::get_recheck_link()
+ */
+ public function test_get_recheck_link() {
+ $this->set_capability();
+ $post_id = $this->create_custom_post();
+ $url = get_edit_post_link( $post_id, 'raw' );
+ $link = AMP_Validation_Utils::get_recheck_link( get_post( $post_id ), $url );
+ $this->assertContains( AMP_Validation_Utils::RECHECK_ACTION, $link );
+ $this->assertContains( wp_create_nonce( AMP_Validation_Utils::NONCE_ACTION . $post_id ), $link );
+ $this->assertContains( 'Recheck the URL for AMP validity', $link );
+ }
+
+ /**
+ * Creates and inserts a custom post.
+ *
+ * @return int|WP_Error $error_post The ID of new custom post, or an error.
+ */
+ public function create_custom_post() {
+ $content = wp_json_encode( $this->get_mock_errors() );
+ $encoded_errors = md5( $content );
+ $post_args = array(
+ 'post_type' => AMP_Validation_Utils::POST_TYPE_SLUG,
+ 'post_name' => $encoded_errors,
+ 'post_content' => $content,
+ 'post_status' => 'publish',
+ );
+ $error_post = wp_insert_post( wp_slash( $post_args ) );
+ $url = home_url( '/' );
+ update_post_meta( $error_post, AMP_Validation_Utils::AMP_URL_META, $url );
+ return $error_post;
+ }
+
+ /**
+ * Gets mock errors for tests.
+ *
+ * @return array $errors[][] {
+ * The data of the validation errors.
+ *
+ * @type string $code Error code.
+ * @type string $node_name Name of removed node.
+ * @type string $parent_name Name of parent node.
+ * @type array[][] $sources Source data, including plugins and themes.
+ * }
+ */
+ public function get_mock_errors() {
+ return array(
+ array(
+ 'code' => AMP_Validation_Utils::INVALID_ELEMENT_CODE,
+ 'node_name' => $this->disallowed_tag_name,
+ 'parent_name' => 'div',
+ 'node_attributes' => array(),
+ 'sources' => array(
+ array(
+ 'type' => 'plugin',
+ 'name' => $this->plugin_name,
+ ),
+ ),
+ ),
+ array(
+ 'code' => AMP_Validation_Utils::INVALID_ATTRIBUTE_CODE,
+ 'node_name' => $this->disallowed_attribute_name,
+ 'parent_name' => 'div',
+ 'element_attributes' => array(
+ $this->disallowed_attribute_name => '',
+ ),
+ 'sources' => array(
+ array(
+ 'type' => 'plugin',
+ 'name' => $this->plugin_name,
+ ),
+ ),
+ ),
+ );
}
}