diff --git a/assets/js/amp-block-validation.js b/assets/js/amp-block-validation.js
index 26cd6bd916e..9146b9164e0 100644
--- a/assets/js/amp-block-validation.js
+++ b/assets/js/amp-block-validation.js
@@ -19,7 +19,8 @@ var ampBlockValidation = ( function() { // eslint-disable-line no-unused-vars
*/
data: {
i18n: {},
- ampValidityRestField: ''
+ ampValidityRestField: '',
+ isCanonical: false
},
/**
@@ -91,10 +92,12 @@ var ampBlockValidation = ( function() { // eslint-disable-line no-unused-vars
/**
* Handle state change regarding validation errors.
*
+ * This is essentially a JS implementation of \AMP_Validation_Manager::print_edit_form_validation_status() in PHP.
+ *
* @return {void}
*/
handleValidationErrorsStateChange: function handleValidationErrorsStateChange() {
- var currentPost, validationErrors, blockValidationErrors, noticeElement, noticeMessage, blockErrorCount, ampValidity;
+ var currentPost, validationErrors, blockValidationErrors, noticeElement, noticeMessage, blockErrorCount, ampValidity, hasActuallyUnacceptedError;
// @todo Gutenberg currently is not persisting isDirty state if changes are made during save request. Block order mismatch.
// We can only align block validation errors with blocks in editor when in saved state, since only here will the blocks be aligned with the validation errors.
@@ -102,11 +105,15 @@ var ampBlockValidation = ( function() { // eslint-disable-line no-unused-vars
return;
}
+ hasActuallyUnacceptedError = false;
currentPost = wp.data.select( 'core/editor' ).getCurrentPost();
ampValidity = currentPost[ module.data.ampValidityRestField ] || {};
validationErrors = _.map(
_.filter( ampValidity.results, function( result ) {
- return ! result.sanitized;
+ if ( result.status !== 1 /* ACCEPTED */ ) {
+ hasActuallyUnacceptedError = true;
+ }
+ return result.term_status !== 1; /* ACCEPTED */
} ),
function( result ) {
return result.error;
@@ -179,7 +186,13 @@ var ampBlockValidation = ( function() { // eslint-disable-line no-unused-vars
);
}
- noticeMessage += ' ' + wp.i18n.__( 'Non-accepted validation errors prevent AMP from being served.', 'amp' );
+ noticeMessage += ' ';
+ if ( hasActuallyUnacceptedError && ! module.data.isCanonical ) {
+ noticeMessage += wp.i18n.__( 'Non-accepted validation errors prevent AMP from being served, and the user will be redirected to the non-AMP version.', 'amp' );
+ } else {
+ noticeMessage += wp.i18n.__( 'The invalid markup will be automatically sanitized to ensure a valid AMP response is served.', 'amp' );
+ }
+
noticeElement = wp.element.createElement( 'p', {}, [
noticeMessage + ' ',
ampValidity.review_link && wp.element.createElement(
diff --git a/includes/admin/class-amp-editor-blocks.php b/includes/admin/class-amp-editor-blocks.php
index a93e396b169..7e880cea840 100644
--- a/includes/admin/class-amp-editor-blocks.php
+++ b/includes/admin/class-amp-editor-blocks.php
@@ -43,8 +43,6 @@ public function init() {
if ( function_exists( 'gutenberg_init' ) ) {
add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ) );
add_filter( 'wp_kses_allowed_html', array( $this, 'whitelist_block_atts_in_wp_kses_allowed_html' ), 10, 2 );
- add_filter( 'the_content', array( $this, 'tally_content_requiring_amp_scripts' ) );
- add_action( 'wp_print_footer_scripts', array( $this, 'print_dirty_amp_scripts' ) );
}
}
@@ -109,27 +107,28 @@ public function whitelist_block_atts_in_wp_kses_allowed_html( $tags, $context )
*/
public function enqueue_block_editor_assets() {
- // Styles.
- wp_enqueue_style(
- 'amp-editor-blocks-style',
- amp_get_asset_url( 'css/amp-editor-blocks.css' ),
- array(),
- AMP__VERSION
- );
+ // Enqueue script and style for AMP-specific blocks.
+ if ( amp_is_canonical() ) {
+ wp_enqueue_style(
+ 'amp-editor-blocks-style',
+ amp_get_asset_url( 'css/amp-editor-blocks.css' ),
+ array(),
+ AMP__VERSION
+ );
- // Scripts.
- wp_enqueue_script(
- 'amp-editor-blocks-build',
- amp_get_asset_url( 'js/amp-blocks-compiled.js' ),
- array( 'wp-blocks', 'lodash', 'wp-i18n', 'wp-element', 'wp-components' ),
- AMP__VERSION
- );
+ wp_enqueue_script(
+ 'amp-editor-blocks-build',
+ amp_get_asset_url( 'js/amp-blocks-compiled.js' ),
+ array( 'wp-blocks', 'lodash', 'wp-i18n', 'wp-element', 'wp-components' ),
+ AMP__VERSION
+ );
- wp_add_inline_script(
- 'amp-editor-blocks-build',
- 'wp.i18n.setLocaleData( ' . wp_json_encode( gutenberg_get_jed_locale_data( 'amp' ) ) . ', "amp" );',
- 'before'
- );
+ wp_add_inline_script(
+ 'amp-editor-blocks-build',
+ 'wp.i18n.setLocaleData( ' . wp_json_encode( gutenberg_get_jed_locale_data( 'amp' ) ) . ', "amp" );',
+ 'before'
+ );
+ }
wp_enqueue_script(
'amp-editor-blocks',
@@ -146,32 +145,4 @@ public function enqueue_block_editor_assets() {
) ) )
);
}
-
- /**
- * Tally the AMP component scripts that are needed in a dirty AMP document.
- *
- * @param string $content Content.
- * @return string Content (unmodified).
- */
- public function tally_content_requiring_amp_scripts( $content ) {
- if ( ! is_amp_endpoint() ) {
- $pattern = sprintf( '/<(%s)\b.*?>/s', join( '|', $this->amp_blocks ) );
- if ( preg_match_all( $pattern, $content, $matches ) ) {
- $this->content_required_amp_scripts = array_merge(
- $this->content_required_amp_scripts,
- $matches[1]
- );
- }
- }
- return $content;
- }
-
- /**
- * Print AMP scripts required for AMP components used in a non-AMP document (dirty AMP).
- */
- public function print_dirty_amp_scripts() {
- if ( ! is_amp_endpoint() && ! empty( $this->content_required_amp_scripts ) ) {
- wp_scripts()->do_items( $this->content_required_amp_scripts );
- }
- }
}
diff --git a/includes/amp-frontend-actions.php b/includes/amp-frontend-actions.php
index 75ce9030a35..fa23028f5a7 100644
--- a/includes/amp-frontend-actions.php
+++ b/includes/amp-frontend-actions.php
@@ -15,6 +15,7 @@
*
* @since 0.2
* @since 1.0 Deprecated
+ * @see amp_add_amphtml_link()
*/
function amp_frontend_add_canonical() {
_deprecated_function( __FUNCTION__, '1.0', 'amp_add_amphtml_link' );
diff --git a/includes/amp-helper-functions.php b/includes/amp-helper-functions.php
index fdb513b6eb0..0bc418687e4 100644
--- a/includes/amp-helper-functions.php
+++ b/includes/amp-helper-functions.php
@@ -169,6 +169,8 @@ function amp_remove_endpoint( $url ) {
/**
* Add amphtml link.
*
+ * If there are known validation errors for the current URL then do not output anything.
+ *
* @since 1.0
*/
function amp_add_amphtml_link() {
@@ -183,12 +185,47 @@ function amp_add_amphtml_link() {
return;
}
+ $current_url = amp_get_current_url();
+
$amp_url = null;
- if ( is_singular() ) {
- $amp_url = amp_get_permalink( get_queried_object_id() );
+ if ( current_theme_supports( 'amp' ) ) {
+ if ( AMP_Theme_Support::is_paired_available() ) {
+ $amp_url = add_query_arg( amp_get_slug(), '', $current_url );
+ }
} else {
- $amp_url = add_query_arg( amp_get_slug(), '', amp_get_current_url() );
+ if ( is_singular() ) {
+ $amp_url = amp_get_permalink( get_queried_object_id() );
+ } else {
+ $amp_url = add_query_arg( amp_get_slug(), '', $current_url );
+ }
+ }
+
+ if ( ! $amp_url ) {
+ printf( '', esc_html__( 'There is no amphtml version available for this URL.', 'amp' ) );
+ return;
}
+
+ // Check to see if there are known unaccepted validation errors for this URL.
+ if ( current_theme_supports( 'amp' ) ) {
+ $validation_errors = AMP_Invalid_URL_Post_Type::get_invalid_url_validation_errors( $current_url, array( 'ignore_accepted' => true ) );
+ $error_count = count( $validation_errors );
+ if ( $error_count > 0 ) {
+ echo "";
+ return;
+ }
+ }
+
if ( $amp_url ) {
printf( '', esc_url( $amp_url ) );
}
diff --git a/includes/class-amp-theme-support.php b/includes/class-amp-theme-support.php
index 9c1ee245c94..a3195b222ff 100644
--- a/includes/class-amp-theme-support.php
+++ b/includes/class-amp-theme-support.php
@@ -96,6 +96,7 @@ class AMP_Theme_Support {
* @since 0.7
*/
public static function init() {
+ self::apply_options();
if ( ! current_theme_supports( 'amp' ) ) {
return;
}
@@ -117,7 +118,7 @@ public static function init() {
$args = array_shift( $support );
if ( ! is_array( $args ) ) {
trigger_error( esc_html__( 'Expected AMP theme support arg to be array.', 'amp' ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
- } elseif ( count( array_diff( array_keys( $args ), array( 'template_dir', 'available_callback', 'comments_live_list' ) ) ) !== 0 ) {
+ } elseif ( count( array_diff( array_keys( $args ), array( 'template_dir', 'available_callback', 'comments_live_list', '__added_via_option' ) ) ) !== 0 ) {
trigger_error( esc_html__( 'Expected AMP theme support to only have template_dir and/or available_callback.', 'amp' ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
}
}
@@ -132,6 +133,28 @@ public static function init() {
add_action( 'wp', array( __CLASS__, 'finish_init' ), PHP_INT_MAX );
}
+ /**
+ * Apply options for whether theme support is enabled via admin and what sanitization is performed by default.
+ *
+ * @see AMP_Post_Type_Support::add_post_type_support() For where post type support is added, since it is irrespective of theme support.
+ */
+ public static function apply_options() {
+ if ( ! current_theme_supports( 'amp' ) ) {
+ $theme_support_option = AMP_Options_Manager::get_option( 'theme_support' );
+ if ( 'disabled' === $theme_support_option ) {
+ return;
+ }
+
+ $args = array(
+ '__added_via_option' => true,
+ );
+ if ( 'paired' === $theme_support_option ) {
+ $args['template_dir'] = './';
+ }
+ add_theme_support( 'amp', $args );
+ }
+ }
+
/**
* Finish initialization once query vars are set.
*
@@ -139,9 +162,7 @@ public static function init() {
*/
public static function finish_init() {
if ( ! is_amp_endpoint() ) {
- if ( self::is_paired_available() ) {
- amp_add_frontend_actions();
- }
+ amp_add_frontend_actions();
return;
}
@@ -1187,7 +1208,10 @@ public static function prepare_response( $response, $args = array() ) {
$dom_serialize_start = microtime( true );
self::ensure_required_markup( $dom );
- if ( ! AMP_Validation_Manager::should_validate_response() && AMP_Validation_Manager::has_blocking_validation_errors() ) {
+ $blocking_error_count = AMP_Validation_Manager::count_blocking_validation_errors();
+ if ( ! AMP_Validation_Manager::should_validate_response() && $blocking_error_count > 0 ) {
+
+ // Note the canonical check will not currently ever be met because dirty AMP is not yet supported; all validation errors will forcibly be sanitized.
if ( amp_is_canonical() ) {
$dom->documentElement->removeAttribute( 'amp' );
@@ -1201,7 +1225,19 @@ public static function prepare_response( $response, $args = array() ) {
$head->appendChild( $script );
}
} else {
- self::redirect_ampless_url( false );
+ $current_url = amp_get_current_url();
+ $ampless_url = amp_remove_endpoint( $current_url );
+ $ampless_url = add_query_arg(
+ AMP_Validation_Manager::VALIDATION_ERRORS_QUERY_VAR,
+ $blocking_error_count,
+ $ampless_url
+ );
+
+ /*
+ * Temporary redirect because AMP URL may return when blocking validation errors
+ * occur or when a non-canonical AMP theme is used.
+ */
+ wp_safe_redirect( $ampless_url, 302 );
return esc_html__( 'Redirecting to non-AMP version.', 'amp' );
}
}
diff --git a/includes/options/class-amp-options-manager.php b/includes/options/class-amp-options-manager.php
index 4461a82f6d8..662521318ce 100644
--- a/includes/options/class-amp-options-manager.php
+++ b/includes/options/class-amp-options-manager.php
@@ -17,6 +17,19 @@ class AMP_Options_Manager {
*/
const OPTION_NAME = 'amp-options';
+ /**
+ * Default option values.
+ *
+ * @var array
+ */
+ protected static $defaults = array(
+ 'theme_support' => 'disabled',
+ 'supported_post_types' => array(),
+ 'analytics' => array(),
+ 'force_sanitization' => false,
+ 'accept_tree_shaking' => false,
+ );
+
/**
* Register settings.
*/
@@ -58,7 +71,11 @@ public static function maybe_flush_rewrite_rules( $old_options, $new_options ) {
* @return array Options.
*/
public static function get_options() {
- return get_option( self::OPTION_NAME, array() );
+ $options = get_option( self::OPTION_NAME, array() );
+ if ( empty( $options ) ) {
+ $options = array();
+ }
+ return array_merge( self::$defaults, $options );
}
/**
@@ -86,15 +103,20 @@ public static function get_option( $option, $default = false ) {
* @return array Options.
*/
public static function validate_options( $new_options ) {
- $defaults = array(
- 'supported_post_types' => array(),
- 'analytics' => array(),
- );
+ $options = self::get_options();
- $options = array_merge(
- $defaults,
- self::get_options()
+ // Theme support.
+ $recognized_theme_supports = array(
+ 'disabled',
+ 'paired',
+ 'native',
);
+ if ( isset( $new_options['theme_support'] ) && in_array( $new_options['theme_support'], $recognized_theme_supports, true ) ) {
+ $options['theme_support'] = $new_options['theme_support'];
+ }
+
+ $options['force_sanitization'] = ! empty( $new_options['force_sanitization'] );
+ $options['accept_tree_shaking'] = ! empty( $new_options['accept_tree_shaking'] );
// Validate post type support.
if ( isset( $new_options['supported_post_types'] ) ) {
@@ -156,7 +178,6 @@ public static function validate_options( $new_options ) {
return $options;
}
-
/**
* Check for errors with updating the supported post types.
*
diff --git a/includes/options/class-amp-options-menu.php b/includes/options/class-amp-options-menu.php
index 3bc556a551f..dd7f18730fc 100644
--- a/includes/options/class-amp-options-menu.php
+++ b/includes/options/class-amp-options-menu.php
@@ -23,6 +23,28 @@ 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' ), 9 );
+
+ $plugin_file = preg_replace( '#.+/(?=.+?/.+?)#', '', AMP__FILE__ );
+ add_filter( "plugin_action_links_{$plugin_file}", array( $this, 'add_plugin_action_links' ) );
+ }
+
+ /**
+ * Add plugin action links.
+ *
+ * @param array $links Links.
+ * @return array Modified links.
+ */
+ public function add_plugin_action_links( $links ) {
+ return array_merge(
+ array(
+ 'settings' => sprintf(
+ '%2$s',
+ esc_url( add_query_arg( 'page', AMP_Options_Manager::OPTION_NAME, admin_url( 'admin.php' ) ) ),
+ __( 'Settings', 'amp' )
+ ),
+ ),
+ $links
+ );
}
/**
@@ -48,17 +70,43 @@ public function add_menu_items() {
);
add_settings_section(
- 'post_types',
+ 'general',
false,
'__return_false',
AMP_Options_Manager::OPTION_NAME
);
+
+ add_settings_field(
+ 'theme_support',
+ __( 'Template Mode', 'amp' ),
+ array( $this, 'render_theme_support' ),
+ AMP_Options_Manager::OPTION_NAME,
+ 'general',
+ array(
+ 'class' => 'theme_support',
+ )
+ );
+
+ add_settings_field(
+ 'validation',
+ __( 'Validation Handling', 'amp' ),
+ array( $this, 'render_validation_handling' ),
+ AMP_Options_Manager::OPTION_NAME,
+ 'general',
+ array(
+ 'class' => 'amp-validation-field',
+ )
+ );
+
add_settings_field(
'supported_post_types',
__( 'Post Type Support', 'amp' ),
array( $this, 'render_post_types_support' ),
AMP_Options_Manager::OPTION_NAME,
- 'post_types'
+ 'general',
+ array(
+ 'class' => 'amp-post-type-support-field',
+ )
);
$submenus = array(
@@ -71,6 +119,148 @@ public function add_menu_items() {
}
}
+ /**
+ * Render theme support.
+ *
+ * @since 1.0
+ */
+ public function render_theme_support() {
+ $theme_support = AMP_Options_Manager::get_option( 'theme_support' );
+
+ $support_args = get_theme_support( 'amp' );
+
+ $theme_support_mutable = (
+ empty( $support_args )
+ ||
+ ! empty( $support_args[0]['__added_via_option'] )
+ );
+ if ( ! $theme_support_mutable ) {
+ if ( amp_is_canonical() ) {
+ $theme_support = 'native';
+ } else {
+ $theme_support = 'paired';
+ }
+ }
+
+ $should_have_theme_support = in_array( get_template(), array( 'twentyfifteen', 'twentysixteen', 'twentyseventeen' ), true );
+ ?>
+
+
+
+
+
diff --git a/includes/sanitizers/class-amp-core-theme-sanitizer.php b/includes/sanitizers/class-amp-core-theme-sanitizer.php
index b07854bb1a7..d645054b1c1 100644
--- a/includes/sanitizers/class-amp-core-theme-sanitizer.php
+++ b/includes/sanitizers/class-amp-core-theme-sanitizer.php
@@ -810,12 +810,17 @@ public function add_nav_menu_toggle( $args = array() ) {
$args
);
- $nav_el = $this->dom->getElementById( $args['nav_container_id'] );
+ $nav_el = $this->dom->getElementById( $args['nav_container_id'] );
+ $button_el = $this->xpath->query( $args['menu_button_xpath'] )->item( 0 );
if ( ! $nav_el ) {
+ if ( $button_el ) {
+
+ // Remove the button since it won't be used.
+ $button_el->parentNode->removeChild( $button_el );
+ }
return;
}
- $button_el = $this->xpath->query( $args['menu_button_xpath'] )->item( 0 );
if ( ! $button_el ) {
return;
}
diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php
index 1b17b2a65e7..1843f0bb06c 100644
--- a/includes/sanitizers/class-amp-style-sanitizer.php
+++ b/includes/sanitizers/class-amp-style-sanitizer.php
@@ -25,6 +25,13 @@
*/
class AMP_Style_Sanitizer extends AMP_Base_Sanitizer {
+ /**
+ * Error code for tree shaking.
+ *
+ * @var string
+ */
+ const TREE_SHAKING_ERROR_CODE = 'removed_unused_css_rules';
+
/**
* Array of flags used to control sanitization.
*
@@ -37,6 +44,7 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer {
* @type callable $validation_error_callback Function to call when a validation error is encountered.
* @type bool $should_locate_sources Whether to locate the sources when reporting validation errors.
* @type string $parsed_cache_variant Additional value by which to vary parsed cache.
+ * @type bool $accept_tree_shaking Whether to accept tree-shaking by default and bypass a validation error.
* }
*/
protected $args;
@@ -56,6 +64,7 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer {
),
'should_locate_sources' => false,
'parsed_cache_variant' => null,
+ 'accept_tree_shaking' => false,
);
/**
@@ -204,7 +213,7 @@ public static function get_css_parser_validation_error_codes() {
'illegal_css_at_rule',
'illegal_css_important',
'illegal_css_property',
- 'removed_unused_css_rules',
+ self::TREE_SHAKING_ERROR_CODE,
'unrecognized_css',
'disallowed_file_extension',
'file_path_not_found',
@@ -1867,9 +1876,9 @@ private function finalize_stylesheet_set( $stylesheet_set ) {
)
);
- if ( $is_too_much_css && $should_tree_shake ) {
+ if ( $is_too_much_css && $should_tree_shake && empty( $this->args['accept_tree_shaking'] ) ) {
$should_tree_shake = $this->should_sanitize_validation_error( array(
- 'code' => 'removed_unused_css_rules',
+ 'code' => self::TREE_SHAKING_ERROR_CODE,
) );
}
diff --git a/includes/validation/class-amp-invalid-url-post-type.php b/includes/validation/class-amp-invalid-url-post-type.php
index f420eb5da5a..93464b8e609 100644
--- a/includes/validation/class-amp-invalid-url-post-type.php
+++ b/includes/validation/class-amp-invalid-url-post-type.php
@@ -24,7 +24,14 @@ class AMP_Invalid_URL_Post_Type {
*
* @var string
*/
- const RECHECK_ACTION = 'amp_recheck';
+ const VALIDATE_ACTION = 'amp_validate';
+
+ /**
+ * The action to bulk recheck URLs for AMP validity.
+ *
+ * @var string
+ */
+ const BULK_VALIDATE_ACTION = 'amp_bulk_validate';
/**
* Action to update the status of AMP validation errors.
@@ -119,7 +126,7 @@ public static function add_admin_hooks() {
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__, 'print_admin_notice' ) );
- add_action( 'post_action_' . self::RECHECK_ACTION, array( __CLASS__, 'handle_inline_recheck' ) );
+ add_action( 'admin_action_' . self::VALIDATE_ACTION, array( __CLASS__, 'handle_validate_request' ) );
add_action( 'post_action_' . self::UPDATE_POST_TERM_STATUS_ACTION, array( __CLASS__, 'handle_validation_error_status_update' ) );
add_action( 'admin_menu', array( __CLASS__, 'add_admin_menu_new_invalid_url_count' ) );
@@ -135,7 +142,7 @@ public static function add_admin_hooks() {
add_filter( 'removable_query_args', function( $query_vars ) {
$query_vars[] = 'amp_actioned';
$query_vars[] = 'amp_taxonomy_terms_updated';
- $query_vars[] = self::REMAINING_ERRORS;
+ $query_vars[] = AMP_Invalid_URL_Post_Type::REMAINING_ERRORS;
$query_vars[] = 'amp_urls_tested';
$query_vars[] = 'amp_validate_error';
return $query_vars;
@@ -172,33 +179,48 @@ public static function add_admin_menu_new_invalid_url_count() {
/**
* Gets validation errors for a given invalid URL post.
*
- * @param int|WP_Post $post Post of amp_invalid_url type.
- * @param array $args {
+ * @param string|int|WP_Post $url Either the URL string or a post (ID or WP_Post) of amp_invalid_url type.
+ * @param array $args {
* Args.
*
* @type bool $ignore_accepted Exclude validation errors that are accepted. Default false.
* }
* @return array List of errors, with keys for term, data, status, and (sanitization) forced.
*/
- public static function get_invalid_url_validation_errors( $post, $args = array() ) {
- $args = array_merge(
+ public static function get_invalid_url_validation_errors( $url, $args = array() ) {
+ $args = array_merge(
array(
'ignore_accepted' => false,
),
$args
);
- $post = get_post( $post );
- $errors = array();
+ // Look up post by URL or ensure the amp_invalid_url object.
+ if ( is_string( $url ) ) {
+ $post = self::get_invalid_url_post( $url );
+ } else {
+ $post = get_post( $url );
+ }
+ if ( ! $post || self::POST_TYPE_SLUG !== $post->post_type ) {
+ return array();
+ }
+
+ // Skip when parse error.
$stored_validation_errors = json_decode( $post->post_content, true );
if ( ! is_array( $stored_validation_errors ) ) {
return array();
}
+
+ $errors = array();
foreach ( $stored_validation_errors as $stored_validation_error ) {
if ( ! isset( $stored_validation_error['term_slug'] ) ) {
continue;
}
+
$term = get_term_by( 'slug', $stored_validation_error['term_slug'], AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG );
+ if ( ! $term ) {
+ continue;
+ }
$sanitization = AMP_Validation_Error_Taxonomy::get_validation_error_sanitization( $stored_validation_error['data'] );
if ( $args['ignore_accepted'] && AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS === $sanitization['status'] ) {
@@ -217,55 +239,51 @@ public static function get_invalid_url_validation_errors( $post, $args = array()
}
/**
- * Get counts for the validation errors associated with a given invalid URL.
+ * Display summary of the validation error counts for a given post.
*
* @param int|WP_Post $post Post of amp_invalid_url type.
- * @return array Term counts.
*/
- public static function get_invalid_url_validation_error_counts( $post ) {
+ public static function display_invalid_url_validation_error_counts_summary( $post ) {
$counts = array_fill_keys(
- array(
- AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS,
- AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS,
- AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECTED_STATUS,
- ),
+ array( 'new', 'accepted', 'rejected' ),
0
);
$validation_errors = self::get_invalid_url_validation_errors( $post );
foreach ( $validation_errors as $error ) {
- $counts[ $error['status'] ]++;
+ switch ( $error['term']->term_group ) {
+ case AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS:
+ $counts['new']++;
+ break;
+ case AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS:
+ $counts['accepted']++;
+ break;
+ case AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECTED_STATUS:
+ $counts['rejected']++;
+ break;
+ }
}
- return $counts;
- }
- /**
- * Display summary of the validation error counts for a given post.
- *
- * @param int|WP_Post $post Post of amp_invalid_url type.
- */
- public static function display_invalid_url_validation_error_counts_summary( $post ) {
$result = array();
- $counts = self::get_invalid_url_validation_error_counts( $post );
- if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS ] ) {
+ if ( $counts['new'] ) {
$result[] = esc_html( sprintf(
/* translators: %s is count */
__( '❓ New: %s', 'amp' ),
- number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS ] )
+ number_format_i18n( $counts['new'] )
) );
}
- if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS ] ) {
+ if ( $counts['accepted'] ) {
$result[] = esc_html( sprintf(
/* translators: %s is count */
__( '✅ Accepted: %s', 'amp' ),
- number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS ] )
+ number_format_i18n( $counts['accepted'] )
) );
}
- if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECTED_STATUS ] ) {
+ if ( $counts['rejected'] ) {
$result[] = esc_html( sprintf(
/* translators: %s is count */
__( '❌ Rejected: %s', 'amp' ),
- number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECTED_STATUS ] )
+ number_format_i18n( $counts['rejected'] )
) );
}
echo implode( '
', $result ); // WPCS: xss ok.
@@ -311,7 +329,7 @@ public static function get_url_from_post( $post ) {
* @param array $validation_errors Validation errors.
* @param string $url URL on which the validation errors occurred. Will be normalized to non-AMP version.
* @param int|WP_Post $post Post to update. Optional. If empty, then post is looked up by URL.
- * @return int|WP_Error $post_id The post ID of the custom post type used, null if post was deleted due to no validation errors, or WP_Error on failure.
+ * @return int|WP_Error $post_id The post ID of the custom post type used, or WP_Error on failure.
* @global WP $wp
*/
public static function store_validation_errors( $validation_errors, $url, $post = null ) {
@@ -326,14 +344,6 @@ public static function store_validation_errors( $validation_errors, $url, $post
}
}
- // Since there are no validation errors and there is an existing $existing_post_id, just delete the post.
- if ( empty( $validation_errors ) ) {
- if ( $post ) {
- wp_delete_post( $post->ID, true );
- }
- return null;
- }
-
/*
* The details for individual validation errors is stored in the amp_validation_error taxonomy terms.
* The post content just contains the slugs for these terms and the sources for the given instance of
@@ -341,23 +351,23 @@ public static function store_validation_errors( $validation_errors, $url, $post
*/
$stored_validation_errors = array();
+ // Prevent Kses from corrupting JSON in description.
+ $has_pre_term_description_filter = has_filter( 'pre_term_description', 'wp_filter_kses' );
+ if ( false !== $has_pre_term_description_filter ) {
+ remove_filter( 'pre_term_description', 'wp_filter_kses', $has_pre_term_description_filter );
+ }
+
$terms = array();
foreach ( $validation_errors as $data ) {
$term_data = AMP_Validation_Error_Taxonomy::prepare_validation_error_taxonomy_term( $data );
$term_slug = $term_data['slug'];
+
if ( ! isset( $terms[ $term_slug ] ) ) {
// Not using WP_Term_Query since more likely individual terms are cached and wp_insert_term() will itself look at this cache anyway.
$term = get_term_by( 'slug', $term_slug, AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG );
if ( ! ( $term instanceof WP_Term ) ) {
- $has_pre_term_description_filter = has_filter( 'pre_term_description', 'wp_filter_kses' );
- if ( false !== $has_pre_term_description_filter ) {
- remove_filter( 'pre_term_description', 'wp_filter_kses', $has_pre_term_description_filter );
- }
$r = wp_insert_term( $term_slug, AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG, wp_slash( $term_data ) );
- if ( false !== $has_pre_term_description_filter ) {
- add_filter( 'pre_term_description', 'wp_filter_kses', $has_pre_term_description_filter );
- }
if ( is_wp_error( $r ) ) {
continue;
}
@@ -371,6 +381,11 @@ public static function store_validation_errors( $validation_errors, $url, $post
$stored_validation_errors[] = compact( 'term_slug', 'data' );
}
+ // Finish preventing Kses from corrupting JSON in description.
+ if ( false !== $has_pre_term_description_filter ) {
+ add_filter( 'pre_term_description', 'wp_filter_kses', $has_pre_term_description_filter );
+ }
+
$post_content = wp_json_encode( $stored_validation_errors );
$placeholder = 'amp_invalid_url_content_placeholder' . wp_rand();
@@ -381,7 +396,7 @@ public static function store_validation_errors( $validation_errors, $url, $post
&&
$placeholder === $post_data['post_content']
&&
- self::POST_TYPE_SLUG === $post_data['post_type']
+ AMP_Invalid_URL_Post_Type::POST_TYPE_SLUG === $post_data['post_type']
);
if ( $should_supply_post_content ) {
$post_data['post_content'] = wp_slash( $post_content );
@@ -648,9 +663,9 @@ public static function filter_row_actions( $actions, $post ) {
);
}
- $actions[ self::RECHECK_ACTION ] = sprintf(
+ $actions[ self::VALIDATE_ACTION ] = sprintf(
'%s',
- esc_url( self::get_recheck_url( $post, get_edit_post_link( $post->ID, 'raw' ) ) ),
+ esc_url( self::get_recheck_url( $post ) ),
esc_html__( 'Re-check', 'amp' )
);
@@ -665,7 +680,7 @@ public static function filter_row_actions( $actions, $post ) {
*/
public static function add_bulk_action( $actions ) {
unset( $actions['edit'] );
- $actions[ self::RECHECK_ACTION ] = esc_html__( 'Recheck', 'amp' );
+ $actions[ self::BULK_VALIDATE_ACTION ] = esc_html__( 'Recheck', 'amp' );
return $actions;
}
@@ -678,7 +693,7 @@ public static function add_bulk_action( $actions ) {
* @return string $redirect The filtered URL of the redirect.
*/
public static function handle_bulk_action( $redirect, $action, $items ) {
- if ( self::RECHECK_ACTION !== $action ) {
+ if ( self::BULK_VALIDATE_ACTION !== $action ) {
return $redirect;
}
$remaining_invalid_urls = array();
@@ -703,7 +718,13 @@ public static function handle_bulk_action( $redirect, $action, $items ) {
}
self::store_validation_errors( $validity['validation_errors'], $validity['url'], $post );
- if ( ! empty( $validity['validation_errors'] ) ) {
+ $unaccepted_error_count = count( array_filter(
+ $validity['validation_errors'],
+ function( $error ) {
+ return ! AMP_Validation_Error_Taxonomy::is_validation_error_sanitized( $error );
+ }
+ ) );
+ if ( $unaccepted_error_count > 0 ) {
$remaining_invalid_urls[] = $validity['url'];
}
}
@@ -761,10 +782,10 @@ public static function print_admin_notice() {
$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 ) {
+ $message = _n( 'The rechecked URL still has unaccepted validation errors.', 'The rechecked URLs still have unaccepted validation errors.', $count_urls_tested, 'amp' );
$class = 'notice-warning';
- $message = _n( 'The rechecked URL still has blocking validation errors.', 'The rechecked URLs still have validation errors.', $count_urls_tested, 'amp' );
} else {
- $message = _n( 'The rechecked URL has no blocking validation errors.', 'The rechecked URLs have no validation errors.', $count_urls_tested, 'amp' );
+ $message = _n( 'The rechecked URL is free of unaccepted validation errors.', 'The rechecked URLs are free of unaccepted validation errors.', $count_urls_tested, 'amp' );
$class = 'updated';
}
@@ -798,41 +819,70 @@ public static function print_admin_notice() {
}
/**
- * Handles clicking 'recheck' on the inline post actions.
+ * Handles clicking 'recheck' on the inline post actions and in the admin bar on the frontend.
*
- * @param int $post_id The post ID of the recheck.
- * @return void
+ * @throws Exception But it is caught. This is here for a PHPCS bug.
*/
- public static function handle_inline_recheck( $post_id ) {
- check_admin_referer( self::NONCE_ACTION . $post_id );
- $validation_results = self::recheck_post( $post_id );
+ public static function handle_validate_request() {
+ check_admin_referer( self::NONCE_ACTION );
+ if ( ! AMP_Validation_Manager::has_cap() ) {
+ wp_die( esc_html__( 'You do not have permissions to validate an AMP URL. Did you get logged out?', 'amp' ) );
+ }
$redirect = wp_get_referer();
- if ( $redirect ) {
- $redirect = remove_query_arg( wp_removable_query_args(), $redirect );
- }
- if ( ! $redirect || empty( $validation_results ) ) {
- // 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',
- );
+ $post = null;
+ $url = null;
- if ( is_wp_error( $validation_results ) ) {
- $args['amp_validate_error'] = $validation_results->get_error_code();
- } else {
- $args[ self::REMAINING_ERRORS ] = count( array_filter(
- $validation_results,
- function( $result ) {
- return ! $result['sanitized'];
+ try {
+ if ( isset( $_GET['post'] ) ) {
+ $post = intval( $_GET['post'] );
+ if ( $post <= 0 ) {
+ throw new Exception( 'unknown_post' );
+ }
+ $post = get_post( $post );
+ if ( ! $post || self::POST_TYPE_SLUG !== $post->post_type ) {
+ throw new Exception( 'invalid_post' );
+ }
+ $url = self::get_url_from_post( $post );
+ } elseif ( isset( $_GET['url'] ) ) {
+ $url = wp_validate_redirect( esc_url_raw( wp_unslash( $_GET['url'] ) ), null );
+ if ( ! $url ) {
+ throw new Exception( 'illegal_url' );
+ }
+ }
+
+ if ( ! $url ) {
+ throw new Exception( 'missing_url' );
+ }
+
+ $validity = AMP_Validation_Manager::validate_url( $url );
+ if ( is_wp_error( $validity ) ) {
+ throw new Exception( esc_html( $validity->get_error_code() ) );
+ }
+
+ $stored = self::store_validation_errors( $validity['validation_errors'], $validity['url'], $post->ID );
+ if ( is_wp_error( $stored ) ) {
+ throw new Exception( esc_html( $stored->get_error_code() ) );
+ }
+ $redirect = get_edit_post_link( $stored, 'raw' );
+
+ $error_count = count( array_filter(
+ $validity['validation_errors'],
+ function ( $error ) {
+ return ! AMP_Validation_Error_Taxonomy::is_validation_error_sanitized( $error );
}
) );
+
+ $args[ self::URLS_TESTED ] = '1';
+ $args[ self::REMAINING_ERRORS ] = $error_count;
+ } catch ( Exception $e ) {
+ $args['amp_validate_error'] = $e->getMessage();
+ $args[ self::URLS_TESTED ] = '0';
+
+ if ( $post ) {
+ $redirect = get_edit_post_link( $post->ID, 'raw' );
+ }
}
wp_safe_redirect( add_query_arg( $args, $redirect ) );
@@ -843,7 +893,7 @@ function( $result ) {
* Re-check invalid URL post for whether it has blocking validation errors.
*
* @param int|WP_Post $post Post.
- * @return array|WP_Error List of blocking validation resukts, or a WP_Error in the case of failure.
+ * @return array|WP_Error List of blocking validation results, or a WP_Error in the case of failure.
*/
public static function recheck_post( $post ) {
$post = get_post( $post );
@@ -960,12 +1010,6 @@ public static function add_meta_boxes() {
* @return void
*/
public static function print_status_meta_box( $post ) {
- $redirect_url = add_query_arg(
- 'post',
- $post->ID,
- admin_url( 'post.php' )
- );
-
?>
-
+
+
+
+
+
+
+
-
-
@@ -1079,6 +1147,7 @@ public static function print_validation_errors_meta_box( $post ) {
}
} );
validatePreviewUrl += '&' + $.param( params );
+ validatePreviewUrl += '#development=1';
window.open( validatePreviewUrl, 'amp-validation-error-term-status-preview-' + String( postId ) );
} );
} );
@@ -1094,9 +1163,9 @@ public static function print_validation_errors_meta_box( $post ) {
}
?>
-
+
@@ -1109,25 +1178,32 @@ public static function print_validation_errors_meta_box( $post ) {
$select_name = sprintf( '%s[%s]', AMP_Validation_Manager::VALIDATION_ERROR_TERM_STATUS_QUERY_VAR, $term->slug );
?>
- >
+ term_group ) ? 'open' : ''; ?>>
-
count > 1 ) : ?>
@@ -1284,19 +1360,22 @@ public static function filter_the_title_in_post_list_table( $title, $post ) {
* 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|WP_Post $url_or_post The post storing the validation error or the URL to check.
* @return string The URL to recheck the post.
*/
- public static function get_recheck_url( $post, $redirect_url ) {
+ public static function get_recheck_url( $url_or_post ) {
+ $args = array(
+ 'action' => self::VALIDATE_ACTION,
+ );
+ if ( is_string( $url_or_post ) ) {
+ $args['url'] = $url_or_post;
+ } elseif ( $url_or_post instanceof WP_Post && self::POST_TYPE_SLUG === $url_or_post->post_type ) {
+ $args['post'] = $url_or_post->ID;
+ }
+
return wp_nonce_url(
- add_query_arg(
- array(
- 'action' => self::RECHECK_ACTION,
- ),
- $redirect_url
- ),
- self::NONCE_ACTION . $post->ID
+ add_query_arg( $args, admin_url() ),
+ self::NONCE_ACTION
);
}
diff --git a/includes/validation/class-amp-validation-error-taxonomy.php b/includes/validation/class-amp-validation-error-taxonomy.php
index 97da59b8f2a..f43d5e63c50 100644
--- a/includes/validation/class-amp-validation-error-taxonomy.php
+++ b/includes/validation/class-amp-validation-error-taxonomy.php
@@ -201,8 +201,9 @@ public static function is_validation_error_sanitized( $error ) {
* @return array {
* Validation error sanitization.
*
- * @type int $status Validation status (0=VALIDATION_ERROR_NEW_STATUS, 1=VALIDATION_ERROR_ACCEPTED_STATUS, 2=VALIDATION_ERROR_REJECTED_STATUS).
- * @type bool $forced Whether sanitization is forced via filter.
+ * @type int $status Validation status (0=VALIDATION_ERROR_NEW_STATUS, 1=VALIDATION_ERROR_ACCEPTED_STATUS, 2=VALIDATION_ERROR_REJECTED_STATUS).
+ * @type int $term_status The initial validation status prior to being overridden by previewing, option, or filter.
+ * @type false|string $forced If and how the status is overridden from its initial term status.
* }
*/
public static function get_validation_error_sanitization( $error ) {
@@ -213,24 +214,43 @@ public static function get_validation_error_sanitization( $error ) {
self::VALIDATION_ERROR_ACCEPTED_STATUS,
self::VALIDATION_ERROR_REJECTED_STATUS,
);
+ if ( ! empty( $term ) && in_array( $term->term_group, $statuses, true ) ) {
+ $term_status = $term->term_group;
+ } else {
+ $term_status = self::VALIDATION_ERROR_NEW_STATUS;
+ }
+
+ $forced = false;
+ $status = $term_status;
+
+ // See note in AMP_Validation_Manager::add_validation_error_sourcing() for why amp_validation_error_sanitized filter isn't used.
if ( isset( AMP_Validation_Manager::$validation_error_status_overrides[ $term_data['slug'] ] ) ) {
- // See note in AMP_Validation_Manager::add_validation_error_sourcing() for why amp_validation_error_sanitized filter isn't used.
$status = AMP_Validation_Manager::$validation_error_status_overrides[ $term_data['slug'] ];
- } elseif ( ! empty( $term ) && in_array( $term->term_group, $statuses, true ) ) {
- $status = $term->term_group;
- } else {
- $status = self::VALIDATION_ERROR_NEW_STATUS;
+ $forced = 'with_preview';
+ }
+
+ $is_forced_with_option = (
+ AMP_Style_Sanitizer::TREE_SHAKING_ERROR_CODE === $error['code'] && AMP_Options_Manager::get_option( 'accept_tree_shaking' )
+ ||
+ AMP_Options_Manager::get_option( 'force_sanitization' )
+ );
+ if ( $is_forced_with_option ) {
+ $forced = 'with_option';
+ $status = self::VALIDATION_ERROR_ACCEPTED_STATUS;
}
/**
* Filters whether the validation error should be sanitized.
*
+ * Returning true this indicates that the validation error is acceptable
+ * and should not be considered a blocker to render AMP. Returning null
+ * means that the default status should be used.
+ *
* Note that the $node is not passed here to ensure that the filter can be
* applied on validation errors that have been stored. Likewise, the $sources
* are also omitted because these are only available during an explicit
* validation request and so they are not suitable for plugins to vary
- * sanitization by. Note that returning false this indicates that the
- * validation error should not be considered a blocker to render AMP.
+ * sanitization by.
*
* @since 1.0
*
@@ -239,13 +259,12 @@ public static function get_validation_error_sanitization( $error ) {
*/
$sanitized = apply_filters( 'amp_validation_error_sanitized', null, $error );
- $forced = false;
if ( null !== $sanitized ) {
- $forced = true;
+ $forced = 'with_filter';
$status = $sanitized ? self::VALIDATION_ERROR_ACCEPTED_STATUS : self::VALIDATION_ERROR_REJECTED_STATUS;
}
- return compact( 'status', 'forced' );
+ return compact( 'status', 'forced', 'term_status' );
}
/**
@@ -254,44 +273,23 @@ public static function get_validation_error_sanitization( $error ) {
* @since 1.0
* @see AMP_Core_Theme_Sanitizer::get_acceptable_errors()
*
- * @param array $acceptable_errors Acceptable validation errors, where keys are codes and values are either `true` or sparse array to check as subset.
+ * @param array|true $acceptable_errors Acceptable validation errors, where keys are codes and values are either `true` or sparse array to check as subset. If just true, then all validation errors are accepted.
*/
public static function accept_validation_errors( $acceptable_errors ) {
if ( empty( $acceptable_errors ) ) {
return;
}
-
- /**
- * Check if one array is a sparse subset of another array.
- *
- * @param array $superset Superset array.
- * @param array $subset Subset array.
- *
- * @return bool Whether subset is contained in superset.
- */
- $is_array_subset = function( $superset, $subset ) use ( &$is_array_subset ) {
- foreach ( $subset as $key => $subset_value ) {
- if ( ! isset( $superset[ $key ] ) || gettype( $subset_value ) !== gettype( $superset[ $key ] ) ) {
- return false;
- }
- if ( is_array( $subset_value ) ) {
- if ( ! $is_array_subset( $superset[ $key ], $subset_value ) ) {
- return false;
- }
- } elseif ( $superset[ $key ] !== $subset_value ) {
- return false;
- }
+ add_filter( 'amp_validation_error_sanitized', function( $sanitized, $error ) use ( $acceptable_errors ) {
+ if ( true === $acceptable_errors ) {
+ return true;
}
- return true;
- };
- add_filter( 'amp_validation_error_sanitized', function( $sanitized, $error ) use ( $is_array_subset, $acceptable_errors ) {
if ( isset( $acceptable_errors[ $error['code'] ] ) ) {
if ( true === $acceptable_errors[ $error['code'] ] ) {
return true;
}
foreach ( $acceptable_errors[ $error['code'] ] as $acceptable_error_props ) {
- if ( $is_array_subset( $error, $acceptable_error_props ) ) {
+ if ( AMP_Validation_Error_Taxonomy::is_array_subset( $error, $acceptable_error_props ) ) {
return true;
}
}
@@ -300,6 +298,30 @@ public static function accept_validation_errors( $acceptable_errors ) {
}, 10, 2 );
}
+ /**
+ * Check if one array is a sparse subset of another array.
+ *
+ * @param array $superset Superset array.
+ * @param array $subset Subset array.
+ *
+ * @return bool Whether subset is contained in superset.
+ */
+ public static function is_array_subset( $superset, $subset ) {
+ foreach ( $subset as $key => $subset_value ) {
+ if ( ! isset( $superset[ $key ] ) || gettype( $subset_value ) !== gettype( $superset[ $key ] ) ) {
+ return false;
+ }
+ if ( is_array( $subset_value ) ) {
+ if ( ! self::is_array_subset( $superset[ $key ], $subset_value ) ) {
+ return false;
+ }
+ } elseif ( $superset[ $key ] !== $subset_value ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Get the count of validation error terms, optionally restricted by term group (e.g. accepted or rejected).
*
@@ -564,20 +586,13 @@ public static function add_group_terms_clauses_filter() {
* @return array All caps.
*/
public static function filter_user_has_cap_for_hiding_term_list_table_checkbox( $allcaps, $caps, $args ) {
+ unset( $caps );
if ( isset( $args[0] ) && 'delete_term' === $args[0] ) {
$term = get_term( $args[2] );
$error = json_decode( $term->description, true );
if ( ! is_array( $error ) ) {
return $allcaps;
}
-
- $sanitization = self::get_validation_error_sanitization( $error );
- if ( $sanitization['forced'] ) {
- $allcaps = array_merge(
- $allcaps,
- array_fill_keys( $caps, false )
- );
- }
}
return $allcaps;
}
@@ -663,29 +678,27 @@ public static function filter_tag_row_actions( $actions, WP_Term $tag ) {
unset( $actions['delete'] );
$sanitization = self::get_validation_error_sanitization( json_decode( $term->description, true ) );
- if ( ! $sanitization['forced'] ) {
- if ( self::VALIDATION_ERROR_REJECTED_STATUS !== $sanitization['status'] ) {
- $actions[ self::VALIDATION_ERROR_REJECT_ACTION ] = sprintf(
- '%s',
- wp_nonce_url(
- add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_REJECT_ACTION ), compact( 'term_id' ) ) ),
- self::VALIDATION_ERROR_REJECT_ACTION
- ),
- esc_attr__( 'Rejecting an error acknowledges that it should block a URL from being served as AMP.', 'amp' ),
- esc_html__( 'Reject', 'amp' )
- );
- }
- if ( self::VALIDATION_ERROR_ACCEPTED_STATUS !== $sanitization['status'] ) {
- $actions[ self::VALIDATION_ERROR_ACCEPT_ACTION ] = sprintf(
- '%s',
- wp_nonce_url(
- add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_ACCEPT_ACTION ), compact( 'term_id' ) ) ),
- self::VALIDATION_ERROR_ACCEPT_ACTION
- ),
- esc_attr__( 'Accepting an error means it will get sanitized and not block a URL from being served as AMP.', 'amp' ),
- esc_html__( 'Accept', 'amp' )
- );
- }
+ if ( self::VALIDATION_ERROR_REJECTED_STATUS !== $sanitization['status'] ) {
+ $actions[ self::VALIDATION_ERROR_REJECT_ACTION ] = sprintf(
+ '%s',
+ wp_nonce_url(
+ add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_REJECT_ACTION ), compact( 'term_id' ) ) ),
+ self::VALIDATION_ERROR_REJECT_ACTION
+ ),
+ esc_attr__( 'Rejecting an error acknowledges that it should block a URL from being served as AMP.', 'amp' ),
+ esc_html__( 'Reject', 'amp' )
+ );
+ }
+ if ( self::VALIDATION_ERROR_ACCEPTED_STATUS !== $sanitization['status'] ) {
+ $actions[ self::VALIDATION_ERROR_ACCEPT_ACTION ] = sprintf(
+ '%s',
+ wp_nonce_url(
+ add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_ACCEPT_ACTION ), compact( 'term_id' ) ) ),
+ self::VALIDATION_ERROR_ACCEPT_ACTION
+ ),
+ esc_attr__( 'Accepting an error means it will get sanitized and not block a URL from being served as AMP.', 'amp' ),
+ esc_html__( 'Accept', 'amp' )
+ );
}
}
return $actions;
@@ -861,10 +874,20 @@ public static function filter_manage_custom_columns( $content, $column_name, $te
break;
case 'status':
$sanitization = self::get_validation_error_sanitization( $validation_error );
- if ( self::VALIDATION_ERROR_ACCEPTED_STATUS === $sanitization['status'] ) {
- $content = '✅ ' . esc_html__( 'Accepted', 'amp' );
- } elseif ( self::VALIDATION_ERROR_REJECTED_STATUS === $sanitization['status'] ) {
- $content = '❌ ' . esc_html__( 'Rejected', 'amp' );
+ if ( self::VALIDATION_ERROR_ACCEPTED_STATUS === $sanitization['term_status'] ) {
+ if ( $sanitization['forced'] && $sanitization['term_status'] !== $sanitization['status'] ) {
+ $content .= '🚩';
+ } else {
+ $content .= '✅';
+ }
+ $content .= ' ' . esc_html__( 'Accepted', 'amp' );
+ } elseif ( self::VALIDATION_ERROR_REJECTED_STATUS === $sanitization['term_status'] ) {
+ if ( $sanitization['forced'] && $sanitization['term_status'] !== $sanitization['status'] ) {
+ $content .= '🚩';
+ } else {
+ $content .= '❌';
+ }
+ $content .= ' ' . esc_html__( 'Rejected', 'amp' );
} else {
$content = '❓ ' . esc_html__( 'New', 'amp' );
}
diff --git a/includes/validation/class-amp-validation-manager.php b/includes/validation/class-amp-validation-manager.php
index f2a8259dd9f..b0f4841092d 100644
--- a/includes/validation/class-amp-validation-manager.php
+++ b/includes/validation/class-amp-validation-manager.php
@@ -19,6 +19,13 @@ class AMP_Validation_Manager {
*/
const VALIDATE_QUERY_VAR = 'amp_validate';
+ /**
+ * Query var for containing the number of validation errors on the frontend after redirection when invalid.
+ *
+ * @var string
+ */
+ const VALIDATION_ERRORS_QUERY_VAR = 'amp_validation_errors';
+
/**
* Query var for passing status preview/update for validation error.
*
@@ -141,8 +148,8 @@ public static function init( $args = array() ) {
self::$should_locate_sources = $args['should_locate_sources'];
- add_action( 'init', array( 'AMP_Invalid_URL_Post_Type', 'register' ) );
- add_action( 'init', array( 'AMP_Validation_Error_Taxonomy', 'register' ) );
+ AMP_Invalid_URL_Post_Type::register();
+ AMP_Validation_Error_Taxonomy::register();
add_action( 'save_post', array( __CLASS__, 'handle_save_post_prompting_validation' ), 10, 2 );
add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_block_validation' ) );
@@ -159,11 +166,145 @@ public static function init( $args = array() ) {
}
} );
+ // Prevent query vars from persisting after redirect.
+ add_filter( 'removable_query_args', function( $query_vars ) {
+ $query_vars[] = AMP_Validation_Manager::VALIDATION_ERRORS_QUERY_VAR;
+ return $query_vars;
+ } );
+
+ add_action( 'admin_bar_menu', array( __CLASS__, 'add_admin_bar_menu_items' ), 100 );
+
+ /*
+ * Set sanitization options on the frontend. These filters get added only on the frontend so that
+ * the user is able to keep track of the errors that they haven't seen before and decide whether
+ * they need to get fixed (rejected) or not (accepted).
+ */
+ add_action( 'template_redirect', function() {
+ if ( AMP_Validation_Manager::is_sanitization_forcibly_accepted() ) {
+ AMP_Validation_Error_Taxonomy::accept_validation_errors( true );
+ } elseif ( AMP_Options_Manager::get_option( 'accept_tree_shaking' ) ) {
+ AMP_Validation_Error_Taxonomy::accept_validation_errors(
+ array(
+ AMP_Style_Sanitizer::TREE_SHAKING_ERROR_CODE => true,
+ )
+ );
+ }
+ } );
+
if ( self::$should_locate_sources ) {
self::add_validation_error_sourcing();
}
}
+ /**
+ * Return whether sanitization is forcibly accepted, whether because in native mode or via user option.
+ *
+ * @return bool Whether sanitization is forcibly accepted.
+ */
+ public static function is_sanitization_forcibly_accepted() {
+ return amp_is_canonical() || AMP_Options_Manager::get_option( 'force_sanitization' );
+ }
+
+ /**
+ * Add menu items to admin bar for AMP.
+ *
+ * @param WP_Admin_Bar $wp_admin_bar Admin bar.
+ */
+ public static function add_admin_bar_menu_items( $wp_admin_bar ) {
+ if ( is_admin() || ! self::has_cap() || ! ( amp_is_canonical() || AMP_Theme_Support::is_paired_available() ) ) {
+ return;
+ }
+
+ $amp_invalid_url_post = null;
+
+ // @todo The amp_invalid_url post should probably only be accessible to users who can manage_options, or limit access to a post if the user has the cap to edit the queried object?
+ $amp_url = amp_get_current_url();
+ $amp_url = remove_query_arg( wp_removable_query_args(), $amp_url );
+ if ( ! amp_is_canonical() ) {
+ $amp_url = add_query_arg( amp_get_slug(), '', $amp_url );
+ }
+
+ $error_count = -1;
+ if ( isset( $_GET[ self::VALIDATION_ERRORS_QUERY_VAR ] ) && is_numeric( $_GET[ self::VALIDATION_ERRORS_QUERY_VAR ] ) ) { // WPCS: CSRF OK.
+ $error_count = intval( $_GET[ self::VALIDATION_ERRORS_QUERY_VAR ] );
+ }
+ if ( $error_count < 0 ) {
+ $amp_invalid_url_post = AMP_Invalid_URL_Post_Type::get_invalid_url_post( $amp_url );
+ if ( $amp_invalid_url_post ) {
+ $error_count = count( AMP_Invalid_URL_Post_Type::get_invalid_url_validation_errors( $amp_invalid_url_post, array( 'ignore_accepted' => true ) ) );
+ }
+ }
+
+ $title = '';
+ if ( $error_count > -1 ) {
+ if ( 0 === $error_count ) {
+ $title .= '✅ ';
+ } else {
+ $title .= '❌ ';
+ }
+ }
+ $title .= 'AMP';
+
+ $wp_admin_bar->add_menu( array(
+ 'id' => 'amp',
+ 'title' => $title,
+ 'href' => esc_url( $amp_url ),
+ ) );
+
+ $wp_admin_bar->add_menu( array(
+ 'parent' => 'amp',
+ 'id' => 'amp-view',
+ 'title' => esc_html__( 'View AMP version', 'amp' ),
+ 'href' => esc_url( $amp_url ),
+ ) );
+
+ if ( $error_count <= 0 ) {
+ $title = esc_html__( 'Re-validate', 'amp' );
+ } else {
+ $title = esc_html(
+ sprintf(
+ /* translators: %s is count of validation errors */
+ _n(
+ 'Re-validate (%s validation error)',
+ 'Re-validate (%s validation errors)',
+ $error_count,
+ 'amp'
+ ),
+ number_format_i18n( $error_count )
+ )
+ );
+ }
+
+ $validate_url = AMP_Invalid_URL_Post_Type::get_recheck_url( $amp_invalid_url_post ? $amp_invalid_url_post : $amp_url );
+
+ $wp_admin_bar->add_menu( array(
+ 'parent' => 'amp',
+ 'id' => 'amp-validity',
+ 'title' => $title,
+ 'href' => esc_url( $validate_url ),
+ ) );
+
+ // Scrub the query var from the URL.
+ if ( ! is_amp_endpoint() && isset( $_GET[ self::VALIDATION_ERRORS_QUERY_VAR ] ) ) { // WPCS: CSRF OK.
+ add_action( 'wp_before_admin_bar_render', function() {
+ ?>
+
+ ID, 'raw' );
foreach ( AMP_Invalid_URL_Post_Type::get_invalid_url_validation_errors( $validation_status_post ) as $result ) {
$field['results'][] = array(
- 'sanitized' => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS === $result['status'],
- 'error' => $result['data'],
+ 'sanitized' => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS === $result['status'],
+ 'error' => $result['data'],
+ 'status' => $result['status'],
+ 'term_status' => $result['term_status'],
+ 'forced' => $result['forced'],
);
}
}
@@ -461,6 +605,8 @@ public static function reset_validation_results() {
*
* If it's not valid AMP, it displays an error message above the 'Classic' editor.
*
+ * This is essentially a PHP implementation of ampBlockValidation.handleValidationErrorsStateChange() in JS.
+ *
* @param WP_Post $post The updated post.
* @return void
*/
@@ -479,10 +625,16 @@ public static function print_edit_form_validation_status( $post ) {
return;
}
- $validation_errors = wp_list_pluck(
- AMP_Invalid_URL_Post_Type::get_invalid_url_validation_errors( $invalid_url_post, array( 'ignore_accepted' => true ) ),
- 'data'
- );
+ $has_actually_unaccepted_error = false;
+ $validation_errors = array();
+ foreach ( AMP_Invalid_URL_Post_Type::get_invalid_url_validation_errors( $invalid_url_post ) as $error ) {
+ if ( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS !== $error['term_status'] ) {
+ $validation_errors[] = $error['data'];
+ if ( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS !== $error['status'] ) {
+ $has_actually_unaccepted_error = true;
+ }
+ }
+ }
// No validation errors so abort.
if ( empty( $validation_errors ) ) {
@@ -491,7 +643,13 @@ public static function print_edit_form_validation_status( $post ) {
echo '';
echo '
';
- esc_html_e( 'There is content which fails AMP validation. Non-accepted validation errors prevent AMP from being served.', 'amp' );
+ esc_html_e( 'There is content which fails AMP validation.', 'amp' );
+ echo ' ';
+ if ( $has_actually_unaccepted_error && ! amp_is_canonical() ) {
+ esc_html_e( 'Non-accepted validation errors prevent AMP from being served, and the user will be redirected to the non-AMP version.', 'amp' );
+ } else {
+ esc_html_e( 'The invalid markup will be automatically sanitized to ensure a valid AMP response is served.', 'amp' );
+ }
echo sprintf(
' %s',
esc_url( get_edit_post_link( $invalid_url_post ) ),
@@ -1182,15 +1340,16 @@ public static function should_validate_response() {
/**
* Determine if there are any validation errors which have not been ignored.
*
- * @return bool Whether AMP is blocked.
+ * @return int Count of errors that block AMP.
*/
- public static function has_blocking_validation_errors() {
+ public static function count_blocking_validation_errors() {
+ $count = 0;
foreach ( self::$validation_results as $result ) {
if ( false === $result['sanitized'] ) {
- return true;
+ $count++;
}
}
- return false;
+ return $count;
}
/**
@@ -1261,6 +1420,7 @@ public static function filter_sanitizer_args( $sanitizers ) {
if ( ! empty( $css_validation_errors ) ) {
$sanitizers['AMP_Style_Sanitizer']['parsed_cache_variant'] = md5( wp_json_encode( $css_validation_errors ) );
}
+ $sanitizers['AMP_Style_Sanitizer']['accept_tree_shaking'] = AMP_Options_Manager::get_option( 'accept_tree_shaking' );
}
return $sanitizers;
@@ -1450,6 +1610,7 @@ public static function enqueue_block_validation() {
$data = wp_json_encode( array(
'i18n' => gutenberg_get_jed_locale_data( 'amp' ), // @todo POT file.
'ampValidityRestField' => self::VALIDITY_REST_FIELD_NAME,
+ 'isCanonical' => amp_is_canonical(),
) );
wp_add_inline_script( $slug, sprintf( 'ampBlockValidation.boot( %s );', $data ) );
}
diff --git a/tests/test-amp-helper-functions.php b/tests/test-amp-helper-functions.php
index 9d3e5a096a7..5df1ddaa730 100644
--- a/tests/test-amp-helper-functions.php
+++ b/tests/test-amp-helper-functions.php
@@ -267,21 +267,47 @@ public function get_amphtml_urls() {
* @param string $amphtml_url The amphtml URL.
*/
public function test_amp_add_amphtml_link( $canonical_url, $amphtml_url ) {
+ $test = $this; // For PHP 5.3.
+
+ $get_amp_html_link = function() {
+ ob_start();
+ amp_add_amphtml_link();
+ return ob_get_clean();
+ };
+
+ $assert_amphtml_link_present = function() use ( $test, $amphtml_url, $get_amp_html_link ) {
+ $test->assertEquals(
+ sprintf( '', esc_url( $amphtml_url ) ),
+ $get_amp_html_link()
+ );
+ };
+
$this->go_to( $canonical_url );
- ob_start();
- amp_add_amphtml_link();
- $output = ob_get_clean();
- $this->assertEquals(
- sprintf( '', esc_url( $amphtml_url ) ),
- $output
- );
+ $assert_amphtml_link_present();
// Make sure adding the filter hides the amphtml link.
add_filter( 'amp_frontend_show_canonical', '__return_false' );
- ob_start();
- amp_add_amphtml_link();
- $output = ob_get_clean();
- $this->assertEmpty( $output );
+ $this->assertEmpty( $get_amp_html_link() );
+ remove_filter( 'amp_frontend_show_canonical', '__return_false' );
+ $assert_amphtml_link_present();
+
+ // Make sure that the link is not provided when there are validation errors associated with the URL.
+ add_theme_support( 'amp', array(
+ 'template_dir' => './',
+ ) );
+ AMP_Theme_Support::init();
+ $invalid_url_post_id = AMP_Invalid_URL_Post_Type::store_validation_errors(
+ array(
+ array( 'code' => 'foo' ),
+ ),
+ $canonical_url
+ );
+ $this->assertNotInstanceOf( 'WP_Error', $invalid_url_post_id );
+
+ // Allow the URL when the errors are forcibly sanitized.
+ $this->assertContains( '