From 2d5a341b578de6d5abd44232c63b929ea24b6371 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 20 Apr 2018 22:37:44 -0700 Subject: [PATCH 01/55] Refactor storage of validation errors to use taxonomy terms --- includes/utils/class-amp-validation-utils.php | 345 ++++++++++++------ templates/admin/amp-status.php | 2 +- tests/test-class-amp-validation-utils.php | 27 ++ 3 files changed, 262 insertions(+), 112 deletions(-) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index 46ea1eb7b4f..a9f8998e14c 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -34,11 +34,18 @@ class AMP_Validation_Utils { const CACHE_BUST_QUERY_VAR = 'amp_cache_bust'; /** - * The slug of the post type to store AMP errors. + * The slug of the post type to store URLs that have AMP errors. * * @var string */ - const POST_TYPE_SLUG = 'amp_validation_error'; + const POST_TYPE_SLUG = 'amp_invalid_url'; + + /** + * The slug of the taxonomy to store AMP errors. + * + * @var string + */ + const TAXONOMY_SLUG = 'amp_validation_error'; /** * The key in the response for the sources that have invalid output. @@ -68,13 +75,6 @@ class AMP_Validation_Utils { */ 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. * @@ -792,11 +792,11 @@ protected static function handle_block_source_comment_replacement( $matches ) { // Obtain source information for block. $source = array( 'block_name' => $matches['name'], - 'post_id' => get_the_ID(), + 'post_id' => get_the_ID(), // @todo This is causing duplicate validation errors to occur when only variance is post_id. ); if ( empty( $matches['closing'] ) ) { - $source['block_content_index'] = self::$block_content_index; + $source['block_content_index'] = self::$block_content_index; // @todo This is causing duplicate validation errors to occur when only variance is post_id. self::$block_content_index++; } @@ -974,7 +974,7 @@ public static function decorate_filter_source( $value ) { 'filter' => true, ); if ( $post ) { - $source['post_id'] = $post->ID; + $source['post_id'] = $post->ID; // @todo This is causing duplicate validation errors to occur when only variance is post_id. $source['post_type'] = $post->post_type; } if ( isset( self::$current_hook_source_stack[ current_filter() ] ) ) { @@ -1287,37 +1287,178 @@ public static function add_validation_callback( $sanitizers ) { /** * Registers the post type to store the validation errors. * - * @return void. + * @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' ), + 'name' => _x( 'Invalid AMP Pages (URLs)', 'post type general name', 'amp' ), + 'menu_name' => __( 'Invalid Pages', 'amp' ), + 'singular_name' => __( 'Invalid AMP Page (URL)', 'amp' ), + 'not_found' => __( 'No invalid AMP pages found', 'amp' ), + 'not_found_in_trash' => __( 'No invalid AMP pages in trash', 'amp' ), + 'search_items' => __( 'Search invalid AMP pages', 'amp' ), + 'edit_item' => __( 'Invalid AMP Page', 'amp' ), ), 'supports' => false, 'public' => false, 'show_ui' => true, 'show_in_menu' => AMP_Options_Manager::OPTION_NAME, + // @todo Show in rest. ) ); // Hide the add new post link. $post_type->cap->create_posts = 'do_not_allow'; + + register_taxonomy( self::TAXONOMY_SLUG, self::POST_TYPE_SLUG, array( + 'labels' => array( + 'name' => _x( 'AMP Validation Errors', 'taxonomy general name', 'amp' ), + 'singular_name' => _x( 'AMP Validation Error', 'taxonomy singular name', 'amp' ), + 'search_items' => __( 'Search AMP Validation Errors', 'amp' ), + 'all_items' => __( 'All AMP Validation Errors', 'amp' ), + 'edit_item' => __( 'Edit AMP Validation Error', 'amp' ), + 'update_item' => __( 'Update AMP Validation Error', 'amp' ), + 'menu_name' => __( 'Validation Errors', 'amp' ), + 'back_to_items' => __( 'Back to AMP Validation Errors', 'amp' ), + 'popular_items' => __( 'Frequent Validation Errors', 'amp' ), + 'view_item' => __( 'View Validation Error', 'amp' ), + 'add_new_item' => __( 'Add New Validation Error', 'amp' ), // Makes no sense. + 'new_item_name' => __( 'New Validation Error Hash', 'amp' ), // Makes no sense. + 'not_found' => __( 'No validation errors found.', 'amp' ), + 'no_terms' => __( 'Validation Error', 'amp' ), + 'items_list_navigation' => __( 'Validation errors navigation', 'amp' ), + 'items_list' => __( 'Validation errors list', 'amp' ), + /* translators: Tab heading when selecting from the most used terms */ + 'most_used' => __( 'Most Used Validation Errors', 'amp' ), + ), + 'public' => false, + 'show_ui' => true, // @todo False because we need a custom UI. + 'show_tagcloud' => false, + 'show_in_quick_edit' => false, + 'hierarchical' => false, // Or true? Code could be the parent term? + 'show_in_menu' => true, + 'meta_box_cb' => false, // See print_validation_errors_meta_box(). + 'capabilities' => array( + 'assign_terms' => 'do_not_allow', + 'edit_terms' => 'do_not_allow', + 'delete_terms' => 'do_not_allow', + ), + ) ); + + // Include searching taxonomy term descriptions. + add_filter( 'terms_clauses', function( $clauses, $taxonomies, $args ) { + global $wpdb; + if ( ! empty( $args['search'] ) && in_array( self::TAXONOMY_SLUG, $taxonomies, true ) ) { + $clauses['where'] = preg_replace( + '#(?<=\()(?=\(t\.name LIKE \')#', + $wpdb->prepare( '(tt.description LIKE %s) OR ', '%' . $wpdb->esc_like( $args['search'] ) . '%' ), + $clauses['where'] + ); + } + return $clauses; + }, 10, 3 ); + + // Hide empty term addition form. + add_action( 'admin_enqueue_scripts', function() { + if ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy ) { + wp_add_inline_style( 'common', '#col-left { display: none; } #col-right { float:none; width: auto; }' ); + } + } ); + + // Show AMP validation errors under AMP admin menu. + add_action( 'admin_menu', function() { + add_submenu_page( + AMP_Options_Manager::OPTION_NAME, + esc_html__( 'Validation Errors', 'amp' ), + esc_html__( 'Validation Errors', 'amp' ), + get_taxonomy( self::TAXONOMY_SLUG )->cap->manage_terms, // Yes, cap is an object not an array. + // The following esc_attr() is sadly needed due to . + esc_attr( 'edit-tags.php?taxonomy=' . self::TAXONOMY_SLUG . '&post_type=' . self::POST_TYPE_SLUG ) + ); + } ); + + // Make sure parent menu item is expanded when visiting the taxonomy term page. + add_filter( 'parent_file', function( $parent_file ) { + if ( get_current_screen()->taxonomy === self::TAXONOMY_SLUG ) { + $parent_file = AMP_Options_Manager::OPTION_NAME; + } + return $parent_file; + }, 10, 2 ); + + // Replace the primary column to be error instead of the removed name column.. + add_filter( 'list_table_primary_column', function( $primary_column ) { + if ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy ) { + $primary_column = 'error'; + } + return $primary_column; + } ); + + // Override the columns displayed for the validation error terms. + add_filter( 'manage_edit-' . self::TAXONOMY_SLUG . '_columns', function( $old_columns ) { + return array_merge( + wp_array_slice_assoc( $old_columns, array( 'cb' ) ), + array( + 'error' => __( 'Error', 'amp' ), + 'details' => __( 'Details', 'amp' ), + 'sources' => __( 'Sources', 'amp' ), + 'posts' => __( 'URLs', 'amp' ), + ) + ); + } ); + + // Supply the content for the custom columns. + add_filter( 'manage_' . self::TAXONOMY_SLUG . '_custom_column', function( $content, $column_name, $term_id ) { + $term = get_term( $term_id ); + + $validation_error = json_decode( $term->description, true ); + if ( ! isset( $validation_error['code'] ) ) { + $validation_error['code'] = 'unknown'; + } + + switch ( $column_name ) { + case 'error': + $content .= '

'; + $content .= sprintf( '%s', esc_html( $validation_error['code'] ) ); + if ( 'invalid_element' === $validation_error['code'] || 'invalid_attribute' === $validation_error['code'] ) { + $content .= sprintf( ': %s', esc_html( $validation_error['node_name'] ) ); + } + $content .= '

'; + + if ( isset( $validation_error['message'] ) ) { + $content .= sprintf( '

%s

', esc_html( $validation_error['message'] ) ); + } + break; + case 'details': + unset( $validation_error['code'] ); + unset( $validation_error['message'] ); + unset( $validation_error['sources'] ); + $content = sprintf( '
%s
', esc_html( wp_json_encode( $validation_error, 128 /* JSON_PRETTY_PRINT */ | 64 /* JSON_UNESCAPED_SLASHES */ ) ) ); + break; + case 'sources': + if ( empty( $validation_error['sources'] ) ) { + $content .= sprintf( '%s', __( 'n/a', 'amp' ) ); + } else { + $content = sprintf( + '
%s
%s
', + number_format_i18n( count( $validation_error['sources'] ) ), + esc_html( wp_json_encode( $validation_error['sources'], 128 /* JSON_PRETTY_PRINT */ | 64 /* JSON_UNESCAPED_SLASHES */ ) ) + ); + } + break; + } + return $content; + }, 10, 3 ); + + // @todo Default to hide_empty terms since we don't want to show errors which don't have any instances on the site. } /** * 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. + * If there are no validation errors provided, then any existing amp_invalid_url post is deleted. * * @param array $validation_errors Validation errors. * @param string $url URL on which the validation errors occurred. @@ -1325,79 +1466,74 @@ public static function register_post_type() { * @global WP $wp */ public static function store_validation_errors( $validation_errors, $url ) { - $post_for_this_url = self::get_validation_status_post( $url ); + $post_slug = md5( $url ); + $post = get_page_by_path( $post_slug, OBJECT, self::POST_TYPE_SLUG ); + if ( ! $post ) { + $post = get_page_by_path( $post_slug . '__trashed', OBJECT, self::POST_TYPE_SLUG ); + } // 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 ); + if ( $post ) { + wp_delete_post( $post->ID, true ); } return null; } - $encoded_errors = wp_json_encode( $validation_errors ); - $post_name = md5( $encoded_errors ); + // Keep track of the original order of the validation errors, and when there are duplicates of a given error. + $ordered_validation_error_hashes = array(); - // 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; - } + $terms = array(); + foreach ( $validation_errors as $data ) { + $description = wp_json_encode( $data ); + $term_slug = md5( $description ); - // 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 ); + 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, self::TAXONOMY_SLUG ); + if ( ! ( $term instanceof WP_Term ) ) { + $r = wp_insert_term( $term_slug, self::TAXONOMY_SLUG, wp_slash( compact( 'description' ) ) ); + if ( is_wp_error( $r ) ) { + continue; + } + $term = get_term( $r['term_id'] ); + } + $terms[ $term_slug ] = $term; } - 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', - ) ), true ); - if ( is_wp_error( $post_id ) ) { - return $post_id; + $ordered_validation_error_hashes[] = $term_slug; } - 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 ); + + // Create a new invalid AMP URL post, or update the existing one. + $r = wp_insert_post( + wp_slash( array( + 'ID' => $post ? $post->ID : null, + 'post_type' => self::POST_TYPE_SLUG, + 'post_title' => $url, + 'post_name' => $post_slug, + 'post_content' => implode( "\n", $ordered_validation_error_hashes ), + 'post_status' => 'publish', // @todo Use draft when doing a post preview? + ) ), + true + ); + if ( is_wp_error( $r ) ) { + return $r; } + $post_id = $r; + wp_set_object_terms( $post_id, wp_list_pluck( $terms, 'term_id' ), self::TAXONOMY_SLUG ); return $post_id; } /** * Gets the existing custom post that stores errors for the $url, if it exists. * + * @todo Rename to get_invalid_url_post(). * @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 ) { - if ( ! post_type_exists( self::POST_TYPE_SLUG ) ) { - return null; - } - $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 ); + return get_page_by_path( md5( $url ), OBJECT, self::POST_TYPE_SLUG ); } /** @@ -1439,7 +1575,7 @@ public static function validate_url( $url ) { ); $r = wp_remote_get( $validation_url, array( - 'cookies' => wp_unslash( $_COOKIE ), + 'cookies' => wp_unslash( $_COOKIE ), // @todo Passing-along the credentials of the currently-authenticated user prevents this from working in cron. 'sslverify' => false, 'headers' => array( 'Cache-Control' => 'no-cache', @@ -1517,7 +1653,6 @@ 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' ), @@ -1546,17 +1681,18 @@ public static function output_custom_column( $column_name, $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; + + $validation_errors = array(); + foreach ( array_filter( explode( "\n", $post->post_content ) ) as $term_slug ) { + $term = get_term_by( 'slug', $term_slug, self::TAXONOMY_SLUG ); + if ( $term ) { + $validation_errors[] = json_decode( $term->description, true ); + } } + $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 ] ); @@ -1605,7 +1741,7 @@ public static function filter_row_actions( $actions, $post ) { esc_html__( 'Details', 'amp' ) ); unset( $actions['inline hide-if-no-js'] ); - $url = get_post_meta( $post->ID, self::AMP_URL_META, true ); + $url = $post->post_title; if ( ! empty( $url ) ) { $actions[ self::RECHECK_ACTION ] = self::get_recheck_link( $post, get_edit_post_link( $post->ID, 'raw' ), $url ); @@ -1646,7 +1782,11 @@ public static function handle_bulk_action( $redirect, $action, $items ) { } $remaining_invalid_urls = array(); foreach ( $items as $item ) { - $url = get_post_meta( $item, self::AMP_URL_META, true ); + $post = get_post( $item ); + if ( empty( $post ) ) { + continue; + } + $url = $post->post_title; if ( empty( $url ) ) { continue; } @@ -1707,7 +1847,8 @@ public static function remaining_error_notice() { */ 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 ); + $post = get_post( $post_id ); + $url = $post->post_title; if ( isset( $_GET['recheck_url'] ) ) { $url = wp_validate_redirect( wp_unslash( $_GET['recheck_url'] ) ); } @@ -1781,7 +1922,7 @@ public static function print_status_meta_box( $post ) { echo '
'; echo self::get_recheck_link( $post, $redirect_url ); // WPCS: XSS ok. - $url = get_post_meta( $post->ID, self::AMP_URL_META, true ); + $url = $post->post_title; if ( $url ) { printf( ' | %s', @@ -1805,16 +1946,19 @@ public static function print_status_meta_box( $post ) { * @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 ); + $errors = array(); + foreach ( array_filter( explode( "\n", $post->post_content ) ) as $term_slug ) { + $term = get_term_by( 'slug', $term_slug, self::TAXONOMY_SLUG ); + if ( $term ) { + $errors[] = json_decode( $term->description, true ); + } + } + ?>
    @@ -1904,27 +2048,6 @@ public static function print_validation_errors_meta_box( $post ) {
-
-

-
    - -
  • - - - 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' ) - ) - ?> - -
  • - -
post type does not support it.', 'amp' ), admin_url( 'admin.php?page=amp-options' ) ) ); + $support_errors[] = wp_kses_post( sprintf( __( 'AMP cannot be enabled because this post type does not support it.', 'amp' ), admin_url( 'admin.php?page=' . AMP_Options_Manager::OPTION_NAME ) ) ); } if ( in_array( 'skip-post', $support_errors_codes, true ) ) { $support_errors[] = __( 'A plugin or theme has disabled AMP support.', 'amp' ); diff --git a/tests/test-class-amp-validation-utils.php b/tests/test-class-amp-validation-utils.php index 9f51954a331..eeef3843a91 100644 --- a/tests/test-class-amp-validation-utils.php +++ b/tests/test-class-amp-validation-utils.php @@ -414,6 +414,8 @@ public function test_print_edit_form_validation_status() { * @covers AMP_Validation_Utils::get_existing_validation_errors() */ public function test_get_existing_validation_errors() { + $this->markTestSkipped( 'Needs rewrite for refactor' ); + add_theme_support( 'amp' ); AMP_Validation_Utils::register_post_type(); $post = $this->factory()->post->create_and_get(); @@ -834,6 +836,8 @@ public function test_add_validation_callback() { * @covers AMP_Validation_Utils::register_post_type() */ public function test_register_post_type() { + $this->markTestSkipped( 'Needs rewrite for refactor' ); + AMP_Validation_Utils::register_post_type(); $amp_post_type = get_post_type_object( AMP_Validation_Utils::POST_TYPE_SLUG ); @@ -853,6 +857,8 @@ public function test_register_post_type() { * @covers AMP_Validation_Utils::store_validation_errors() */ public function test_store_validation_errors() { + $this->markTestSkipped( 'Needs rewrite for refactor' ); + global $post; $post = $this->factory()->post->create_and_get(); // WPCS: global override ok. add_theme_support( 'amp' ); @@ -938,6 +944,8 @@ public function test_store_validation_errors() { * @covers AMP_Validation_Utils::store_validation_errors() */ public function test_store_validation_errors_untrashing() { + $this->markTestSkipped( 'Needs rewrite for refactor' ); + $validation_errors = $this->get_mock_errors(); $first_post_id = AMP_Validation_Utils::store_validation_errors( $validation_errors, home_url( '/foo/' ) ); @@ -967,6 +975,8 @@ public function test_store_validation_errors_untrashing() { * @covers AMP_Validation_Utils::get_validation_status_post() */ public function test_get_validation_status_post() { + $this->markTestSkipped( 'Needs rewrite for refactor' ); + global $post; $post = $this->factory()->post->create_and_get(); // WPCS: global override ok. $custom_post_id = $this->factory()->post->create( array( @@ -1106,6 +1116,8 @@ public function test_plugin_notice() { * @covers AMP_Validation_Utils::add_post_columns() */ public function test_add_post_columns() { + $this->markTestSkipped( 'Needs rewrite for refactor' ); + $initial_columns = array( 'cb' => '', ); @@ -1132,6 +1144,8 @@ public function test_add_post_columns() { * @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 ) { + $this->markTestSkipped( 'Needs rewrite for refactor' ); + ob_start(); AMP_Validation_Utils::output_custom_column( $column_name, $this->create_custom_post() ); $this->assertContains( $expected_value, ob_get_clean() ); @@ -1169,6 +1183,8 @@ public function get_custom_columns() { * @covers AMP_Validation_Utils::filter_row_actions() */ public function test_filter_row_actions() { + $this->markTestSkipped( 'Needs rewrite for refactor' ); + $this->set_capability(); $initial_actions = array( @@ -1204,6 +1220,8 @@ public function test_add_bulk_action() { * @covers AMP_Validation_Utils::handle_bulk_action() */ public function test_handle_bulk_action() { + $this->markTestSkipped( 'Needs rewrite for refactor' ); + $initial_redirect = admin_url( 'plugins.php' ); $items = array( $this->create_custom_post() ); $urls_tested = '1'; @@ -1283,6 +1301,8 @@ public function test_remaining_error_notice() { * @covers AMP_Validation_Utils::handle_inline_recheck() */ public function test_handle_inline_recheck() { + $this->markTestSkipped( 'Needs rewrite for refactor' ); + $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( @@ -1350,6 +1370,8 @@ public function test_add_meta_boxes() { * @covers AMP_Validation_Utils::print_status_meta_box() */ public function test_print_status_meta_box() { + $this->markTestSkipped( 'Needs rewrite for refactor' ); + $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 ); @@ -1382,6 +1404,7 @@ public function test_print_status_meta_box() { * @covers AMP_Validation_Utils::print_status_meta_box() */ public function test_print_validation_errors_meta_box() { + $this->markTestSkipped( 'Needs rewrite for refactor' ); $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 ); @@ -1417,6 +1440,8 @@ public function test_get_debug_url() { * @covers AMP_Validation_Utils::get_recheck_link() */ public function test_get_recheck_link() { + $this->markTestSkipped( 'Needs rewrite for refactor' ); + $this->set_capability(); $post_id = $this->create_custom_post(); $url = get_edit_post_link( $post_id, 'raw' ); @@ -1503,6 +1528,8 @@ public function assert_rest_api_field_present( $post_types ) { * @covers AMP_Validation_Utils::get_amp_validity_rest_field() */ public function test_rest_field_amp_validation() { + $this->markTestSkipped( 'Needs rewrite for refactor' ); + AMP_Validation_Utils::register_post_type(); $id = $this->factory()->post->create(); $this->assertNull( AMP_Validation_Utils::get_amp_validity_rest_field( From 1105fe33d3a5fd8677ac88a060416135614eba9d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 21 Apr 2018 12:05:10 -0700 Subject: [PATCH 02/55] Add acknowledged/ignored statuses for validation errors --- includes/utils/class-amp-validation-utils.php | 196 +++++++++++++++++- 1 file changed, 187 insertions(+), 9 deletions(-) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index a9f8998e14c..b159ec86b7e 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -47,6 +47,41 @@ class AMP_Validation_Utils { */ const TAXONOMY_SLUG = 'amp_validation_error'; + /** + * Term group for validation_error terms have not yet been acknowledged. + * + * @var int + */ + const VALIDATION_ERROR_NEW_STATUS = 0; + + /** + * Term group for validation_error terms that the user acknowledges as being ignored (and thus not disabling AMP). + * + * @var int + */ + const VALIDATION_ERROR_IGNORED_STATUS = 1; + + /** + * Action name for ignoring a validation error. + * + * @var string + */ + const VALIDATION_ERROR_IGNORE_ACTION = 'amp_validation_error_ignore'; + + /** + * Action name for acknowledging a validation error. + * + * @var string + */ + const VALIDATION_ERROR_ACKNOWLEDGE_ACTION = 'amp_validation_error_acknowledge'; + + /** + * Term group for validation_error terms that the user acknowledges (as being blockers to enabling AMP). + * + * @var int + */ + const VALIDATION_ERROR_ACKNOWLEDGED_STATUS = 2; + /** * The key in the response for the sources that have invalid output. * @@ -1344,7 +1379,7 @@ public static function register_post_type() { 'capabilities' => array( 'assign_terms' => 'do_not_allow', 'edit_terms' => 'do_not_allow', - 'delete_terms' => 'do_not_allow', + // Note that delete_terms is needed so the checkbox (cb) table column will work. ), ) ); @@ -1398,14 +1433,13 @@ public static function register_post_type() { // Override the columns displayed for the validation error terms. add_filter( 'manage_edit-' . self::TAXONOMY_SLUG . '_columns', function( $old_columns ) { - return array_merge( - wp_array_slice_assoc( $old_columns, array( 'cb' ) ), - array( - 'error' => __( 'Error', 'amp' ), - 'details' => __( 'Details', 'amp' ), - 'sources' => __( 'Sources', 'amp' ), - 'posts' => __( 'URLs', 'amp' ), - ) + return array( + 'cb' => $old_columns['cb'], + 'error' => __( 'Error', 'amp' ), + 'status' => __( 'Status', 'amp' ), + 'details' => __( 'Details', 'amp' ), + 'sources' => __( 'Sources', 'amp' ), + 'posts' => __( 'URLs', 'amp' ), ); } ); @@ -1431,6 +1465,15 @@ public static function register_post_type() { $content .= sprintf( '

%s

', esc_html( $validation_error['message'] ) ); } break; + case 'status': + if ( self::VALIDATION_ERROR_IGNORED_STATUS === $term->term_group ) { + $content = esc_html__( 'Ignored', 'amp' ); + } elseif ( self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS === $term->term_group ) { + $content = esc_html__( 'Acknowledged', 'amp' ); + } else { + $content = esc_html__( 'New', 'amp' ); + } + break; case 'details': unset( $validation_error['code'] ); unset( $validation_error['message'] ); @@ -1452,9 +1495,144 @@ public static function register_post_type() { return $content; }, 10, 3 ); + // Add row actions. + add_filter( 'tag_row_actions', function( $actions, WP_Term $tag ) { + if ( self::TAXONOMY_SLUG === $tag->taxonomy ) { + unset( $actions['delete'] ); + $term_id = $tag->term_id; + if ( self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS !== $tag->term_group ) { + $actions[ self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = sprintf( + '%s', + wp_nonce_url( + add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ), compact( 'term_id' ) ) ), + self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION + ), + esc_attr__( 'Acknowledging an error marks it as read. AMP validation errors prevent a URL from being served as AMP.', 'amp' ), + esc_html__( 'Acknowledge', 'amp' ) + ); + } + if ( self::VALIDATION_ERROR_IGNORED_STATUS !== $tag->term_group ) { + $actions[ self::VALIDATION_ERROR_IGNORE_ACTION ] = sprintf( + '%s', + wp_nonce_url( + add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_IGNORE_ACTION ), compact( 'term_id' ) ) ), + self::VALIDATION_ERROR_IGNORE_ACTION + ), + esc_attr__( 'Ignoring an error prevents it from blocking a URL from being served as AMP.', 'amp' ), + esc_html__( 'Ignore', 'amp' ) + ); + } + } + return $actions; + }, 10, 2 ); + + // Handle inline edit links. + add_action( 'load-edit-tags.php', function() { + if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || ! isset( $_GET['action'] ) || ! isset( $_GET['_wpnonce'] ) || ! isset( $_GET['term_id'] ) ) { // WPCS: CSRF ok. + return; + } + $action = sanitize_key( $_GET['action'] ); // WPCS: CSRF ok. + check_admin_referer( $action ); + $tax = get_taxonomy( self::TAXONOMY_SLUG ); + if ( ! current_user_can( $tax->cap->manage_terms ) ) { // Yes it is an object. + return; + } + + $referer = wp_get_referer(); + $term_id = intval( $_GET['term_id'] ); // WPCS: CSRF ok. + $redirect = self::handle_validation_error_update( $referer, $action, array( $term_id ) ); + if ( $redirect !== $referer ) { + $redirect = remove_query_arg( array( 'action', '_wpnonce', 'term_id' ), $redirect ); + wp_safe_redirect( $redirect ); + exit; + } + } ); + + // Add bulk actions. + add_filter( 'bulk_actions-edit-' . self::TAXONOMY_SLUG, function( $bulk_actions ) { + unset( $bulk_actions['delete'] ); + $bulk_actions[ self::VALIDATION_ERROR_IGNORE_ACTION ] = __( 'Ignore', 'amp' ); + $bulk_actions[ self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = __( 'Acknowledge', 'amp' ); + return $bulk_actions; + } ); + + // Handle bulk actions. + add_filter( 'handle_bulk_actions-edit-' . self::TAXONOMY_SLUG, array( __CLASS__, 'handle_validation_error_update' ), 10, 3 ); + + // Show notices for bulk actions. + add_action( 'admin_notices', function() { + if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || empty( $_GET['amp_actioned'] ) || empty( $_GET['actioned_count'] ) ) { // WPCS: CSRF ok. + return; + } + $actioned = sanitize_key( $_GET['amp_actioned'] ); // WPCS: CSRF ok. + $count = intval( $_GET['actioned_count'] ); // WPCS: CSRF ok. + $message = null; + if ( self::VALIDATION_ERROR_IGNORE_ACTION === $actioned ) { + $message = sprintf( + /* translators: %s is number of errors ignored */ + _n( + 'Ignored %s error. It will no longer block related URLs from being served as AMP.', + 'Ignored %s errors. They will no longer block related URLs from being served as AMP.', + number_format_i18n( $count ), + 'amp' + ), + $count + ); + } elseif ( self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION === $actioned ) { + $message = sprintf( + /* translators: %s is number of errors acknowledged */ + _n( + 'Acknowledged %s error. It will continue to block related URLs from being served as AMP.', + 'Acknowledged %s errors. They will continue to block related URLs from being served as AMP.', + number_format_i18n( $count ), + 'amp' + ), + $count + ); + } + + if ( $message ) { + printf( '

%s

', esc_html( $message ) ); + } + } ); + + // @todo Be able to filter validation errors by new, acknowledged, or ignored. // @todo Default to hide_empty terms since we don't want to show errors which don't have any instances on the site. } + /** + * Handle bulk and inline edits to amp_validation_error terms. + * + * @param string $redirect_to Redirect to. + * @param string $action Action. + * @param int[] $term_ids Term IDs. + * + * @return string Redirect. + */ + public static function handle_validation_error_update( $redirect_to, $action, $term_ids ) { + $term_group = null; + if ( self::VALIDATION_ERROR_IGNORE_ACTION === $action ) { + $term_group = self::VALIDATION_ERROR_IGNORED_STATUS; + } elseif ( self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION === $action ) { + $term_group = self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS; + } + + if ( $term_group ) { + foreach ( $term_ids as $term_id ) { + wp_update_term( $term_id, self::TAXONOMY_SLUG, compact( 'term_group' ) ); + } + $redirect_to = add_query_arg( + array( + 'amp_actioned' => $action, + 'actioned_count' => count( $term_ids ), + ), + $redirect_to + ); + } + + return $redirect_to; + } + /** * Stores the validation errors. * From 36cf381104ccd735e783cb26f6fc8688c1d31ef8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 21 Apr 2018 23:18:10 -0700 Subject: [PATCH 03/55] Add filtering of validation errors by status * Add get_validation_error_count() method. * Add removable_query_args to prevent persisting on page --- includes/utils/class-amp-validation-utils.php | 189 +++++++++++++++++- 1 file changed, 187 insertions(+), 2 deletions(-) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index b159ec86b7e..0898c4a5744 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -47,6 +47,13 @@ class AMP_Validation_Utils { */ const TAXONOMY_SLUG = 'amp_validation_error'; + /** + * Query var used when filtering by validation error status. + * + * @var string + */ + const VALIDATION_ERROR_STATUS_QUERY_VAR = 'amp_validation_error_status'; + /** * Term group for validation_error terms have not yet been acknowledged. * @@ -247,6 +254,16 @@ class AMP_Validation_Utils { */ public static $hook_source_stack = array(); + /** + * Whether the terms_clauses filter should apply to a term query for validation errors to limit to a given status. + * + * This is set to false when calling wp_count_terms() for the admin menu and for the views. + * + * @see AMP_Validation_Utils::get_validation_error_count() + * @var bool + */ + protected static $should_filter_terms_clauses_for_error_validation_status; + /** * Add the actions. * @@ -286,6 +303,8 @@ public static function init() { /** * Add count of how many validation error posts there are to the admin menu. + * + * @todo This probably needs to be updated to show the number of amp_invalid_url posts which have validation errors in the new group. */ public static function add_admin_menu_validation_status_count() { global $submenu; @@ -1405,10 +1424,16 @@ public static function register_post_type() { // Show AMP validation errors under AMP admin menu. add_action( 'admin_menu', function() { + $menu_item_label = esc_html__( 'Validation Errors', 'amp' ); + $new_error_count = self::get_validation_error_count( self::VALIDATION_ERROR_NEW_STATUS ); + if ( $new_error_count ) { + $menu_item_label .= ' ' . esc_html( number_format_i18n( $new_error_count ) ) . ''; + } + add_submenu_page( AMP_Options_Manager::OPTION_NAME, esc_html__( 'Validation Errors', 'amp' ), - esc_html__( 'Validation Errors', 'amp' ), + $menu_item_label, get_taxonomy( self::TAXONOMY_SLUG )->cap->manage_terms, // Yes, cap is an object not an array. // The following esc_attr() is sadly needed due to . esc_attr( 'edit-tags.php?taxonomy=' . self::TAXONOMY_SLUG . '&post_type=' . self::POST_TYPE_SLUG ) @@ -1431,6 +1456,117 @@ public static function register_post_type() { return $primary_column; } ); + // Add views for filtering validation errors by status. + add_filter( 'views_edit-' . self::TAXONOMY_SLUG, function( $views ) { + $total_term_count = self::get_validation_error_count(); + $acknowledged_term_count = self::get_validation_error_count( self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ); + $ignored_term_count = self::get_validation_error_count( self::VALIDATION_ERROR_IGNORED_STATUS ); + $new_term_count = $total_term_count - $acknowledged_term_count - $ignored_term_count; + + $current_url = remove_query_arg( + array_merge( + wp_removable_query_args(), + array( 's' ) // For some reason behavior of posts list table is to not persist the search query. + ), + wp_unslash( $_SERVER['REQUEST_URI'] ) + ); + + $current_status = null; + if ( isset( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ) ) { // WPCS: CSRF ok. + $value = intval( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ); // WPCS: CSRF ok. + if ( in_array( $value, array( self::VALIDATION_ERROR_NEW_STATUS, self::VALIDATION_ERROR_IGNORED_STATUS, self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ), true ) ) { + $current_status = $value; + } + } + + $views['all'] = sprintf( + '%s', + esc_url( remove_query_arg( self::VALIDATION_ERROR_STATUS_QUERY_VAR, $current_url ) ), + null === $current_status ? 'current' : '', + sprintf( + /* translators: %s is the term count */ + _nx( + 'All (%s)', + 'All (%s)', + $total_term_count, + 'terms', + 'amp' + ), + number_format_i18n( $total_term_count ) + ) + ); + + $views['new'] = sprintf( + '%s', + esc_url( + add_query_arg( + self::VALIDATION_ERROR_STATUS_QUERY_VAR, + self::VALIDATION_ERROR_NEW_STATUS, + $current_url + ) + ), + self::VALIDATION_ERROR_NEW_STATUS === $current_status ? 'current' : '', + sprintf( + /* translators: %s is the term count */ + _nx( + 'New (%s)', + 'New (%s)', + $new_term_count, + 'terms', + 'amp' + ), + number_format_i18n( $new_term_count ) + ) + ); + + $views['acknowledged'] = sprintf( + '%s', + esc_url( + add_query_arg( + self::VALIDATION_ERROR_STATUS_QUERY_VAR, + self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS, + $current_url + ) + ), + self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS === $current_status ? 'current' : '', + sprintf( + /* translators: %s is the term count */ + _nx( + 'Acknowledged (%s)', + 'Acknowledged (%s)', + $acknowledged_term_count, + 'terms', + 'amp' + ), + number_format_i18n( $acknowledged_term_count ) + ) + ); + + $views['ignored'] = sprintf( + '%s', + esc_url( + add_query_arg( + self::VALIDATION_ERROR_STATUS_QUERY_VAR, + self::VALIDATION_ERROR_IGNORED_STATUS, + $current_url + ) + ), + self::VALIDATION_ERROR_IGNORED_STATUS === $current_status ? 'current' : '', + sprintf( + /* translators: %s is the term count */ + _nx( + 'Ignored (%s)', + 'Ignored (%s)', + $ignored_term_count, + 'terms', + 'amp' + ), + number_format_i18n( $ignored_term_count ) + ) + ); + return $views; + } ); + // Override the columns displayed for the validation error terms. add_filter( 'manage_edit-' . self::TAXONOMY_SLUG . '_columns', function( $old_columns ) { return array( @@ -1526,6 +1662,25 @@ public static function register_post_type() { return $actions; }, 10, 2 ); + // Filter amp_validation_error term query by term group when requested. + add_action( 'load-edit-tags.php', function() { + if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || ! isset( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ) ) { // WPCS: CSRF ok. + return; + } + self::$should_filter_terms_clauses_for_error_validation_status = true; + $group = intval( $_GET[ AMP_Validation_Utils::VALIDATION_ERROR_STATUS_QUERY_VAR ] ); // WPCS: CSRF ok. + if ( ! in_array( $group, array( self::VALIDATION_ERROR_NEW_STATUS, self::VALIDATION_ERROR_IGNORED_STATUS, self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ), true ) ) { + return; + } + add_filter( 'terms_clauses', function( $clauses, $taxonomies ) use ( $group ) { + global $wpdb; + if ( self::TAXONOMY_SLUG === $taxonomies[0] && self::$should_filter_terms_clauses_for_error_validation_status ) { + $clauses['where'] .= $wpdb->prepare( ' AND t.term_group = %d', $group ); + } + return $clauses; + }, 10, 2 ); + } ); + // Handle inline edit links. add_action( 'load-edit-tags.php', function() { if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || ! isset( $_GET['action'] ) || ! isset( $_GET['_wpnonce'] ) || ! isset( $_GET['term_id'] ) ) { // WPCS: CSRF ok. @@ -1559,6 +1714,13 @@ public static function register_post_type() { // Handle bulk actions. add_filter( 'handle_bulk_actions-edit-' . self::TAXONOMY_SLUG, array( __CLASS__, 'handle_validation_error_update' ), 10, 3 ); + // Prevent query vars from persisting after redirect. + add_filter( 'removable_query_args', function( $query_vars ) { + $query_vars[] = 'amp_actioned'; + $query_vars[] = 'actioned_count'; + return $query_vars; + } ); + // Show notices for bulk actions. add_action( 'admin_notices', function() { if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || empty( $_GET['amp_actioned'] ) || empty( $_GET['actioned_count'] ) ) { // WPCS: CSRF ok. @@ -1596,10 +1758,33 @@ public static function register_post_type() { } } ); - // @todo Be able to filter validation errors by new, acknowledged, or ignored. // @todo Default to hide_empty terms since we don't want to show errors which don't have any instances on the site. } + /** + * Get the count of validation error terms, optionally restricted by term group (e.g. ignored or acknowledged). + * + * @param int|null $group Term group. + * @return int|WP_Error Number of terms in that taxonomy or WP_Error if the taxonomy does not exist. + */ + public static function get_validation_error_count( $group = null ) { + $filter = function( $clauses ) use ( $group ) { + global $wpdb; + $clauses['where'] .= $wpdb->prepare( ' AND t.term_group = %d', $group ); + return $clauses; + }; + if ( isset( $group ) ) { + add_filter( 'terms_clauses', $filter ); + } + self::$should_filter_terms_clauses_for_error_validation_status = false; + $term_count = wp_count_terms( self::TAXONOMY_SLUG ); + self::$should_filter_terms_clauses_for_error_validation_status = true; + if ( isset( $group ) ) { + remove_filter( 'terms_clauses', $filter ); + } + return $term_count; + } + /** * Handle bulk and inline edits to amp_validation_error terms. * From 044d3c2cbf063cff470793bfe10ccd8d68555e52 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 22 Apr 2018 21:10:38 -0700 Subject: [PATCH 04/55] Add views for filtering invalid URLs by which contain specific error statuses * Add recognition of amp_validation_error_status query var for amp_invalid_url post queries. * Indicate in the list of errors on a given invalid URL post which are new, ignored, or acknowledged. --- includes/utils/class-amp-validation-utils.php | 235 ++++++++++++++++-- 1 file changed, 212 insertions(+), 23 deletions(-) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index 0898c4a5744..1478e2c3c60 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -323,6 +323,43 @@ public static function add_admin_menu_validation_status_count() { } } + /** + * Add recognition of amp_validation_error_status query var for amp_invalid_url post queries. + * + * @see WP_Tax_Query::get_sql_for_clause() + * + * @param string $where SQL WHERE clause. + * @param WP_Query $query Query. + * @return string Modified WHERE clause. + */ + public static function filter_posts_where_for_validation_error_status( $where, WP_Query $query ) { + global $wpdb; + if ( + in_array( self::POST_TYPE_SLUG, (array) $query->get( 'post_type' ), true ) + && + is_numeric( $query->get( self::VALIDATION_ERROR_STATUS_QUERY_VAR ) ) + ) { + $where .= $wpdb->prepare( + " AND ( + SELECT 1 + FROM $wpdb->term_relationships + INNER JOIN $wpdb->term_taxonomy ON $wpdb->term_taxonomy.term_taxonomy_id = $wpdb->term_relationships.term_taxonomy_id + INNER JOIN $wpdb->terms ON $wpdb->terms.term_id = $wpdb->term_taxonomy.term_id + WHERE + $wpdb->term_taxonomy.taxonomy = %s + AND + $wpdb->term_relationships.object_id = $wpdb->posts.ID + AND + $wpdb->terms.term_group = %d + LIMIT 1 + )", + self::TAXONOMY_SLUG, + $query->get( self::VALIDATION_ERROR_STATUS_QUERY_VAR ) + ); + } + return $where; + } + /** * Filter At a Glance items add AMP Validation Errors. * @@ -330,15 +367,35 @@ public static function add_admin_menu_validation_status_count() { * @return array Items. */ public static function filter_dashboard_glance_items( $items ) { - $counts = wp_count_posts( self::POST_TYPE_SLUG ); - if ( ! empty( $counts->publish ) ) { + + $query = new WP_Query( array( + 'post_type' => self::POST_TYPE_SLUG, + self::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_NEW_STATUS, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) ); + + if ( 0 !== $query->found_posts ) { $items[] = sprintf( '%s', - esc_url( admin_url( 'edit.php?post_type=' . self::POST_TYPE_SLUG ) ), + esc_url( admin_url( + add_query_arg( + array( + 'post_type' => self::POST_TYPE_SLUG, + self::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_NEW_STATUS, + ), + 'edit.php' + ) + ) ), esc_html( sprintf( /* translators: %s is the validation error count */ - _n( '%s AMP Validation Error', '%s AMP Validation Errors', $counts->publish, 'amp' ), - $counts->publish + _n( + '%s URL w/ new AMP errors', + '%s URLs w/ new AMP errors', + $query->found_posts, + 'amp' + ), + $query->found_posts ) ) ); } @@ -1402,6 +1459,15 @@ public static function register_post_type() { ), ) ); + // Add support for querying posts by amp_validation_error_status. + add_filter( 'posts_where', array( __CLASS__, 'filter_posts_where_for_validation_error_status' ), 10, 2 ); + + // Add recognition of amp_validation_error_status query var (which will only apply in admin since post type is not publicly_queryable). + add_filter( 'query_vars', function( $query_vars ) { + $query_vars[] = self::VALIDATION_ERROR_STATUS_QUERY_VAR; + return $query_vars; + } ); + // Include searching taxonomy term descriptions. add_filter( 'terms_clauses', function( $clauses, $taxonomies, $args ) { global $wpdb; @@ -1456,6 +1522,117 @@ public static function register_post_type() { return $primary_column; } ); + // Add views for filtering validation errors by status. + add_filter( 'views_edit-' . self::POST_TYPE_SLUG, function( $views ) { + unset( $views['publish'] ); + + $args = array( + 'post_type' => self::POST_TYPE_SLUG, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ); + + $with_new_query = new WP_Query( array_merge( + $args, + array( self::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_NEW_STATUS ) + ) ); + $with_acknowledged_query = new WP_Query( array_merge( + $args, + array( self::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ) + ) ); + $with_ignored_query = new WP_Query( array_merge( + $args, + array( self::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_IGNORED_STATUS ) + ) ); + + $current_url = remove_query_arg( + array_merge( + wp_removable_query_args(), + array( 's' ) // For some reason behavior of posts list table is to not persist the search query. + ), + wp_unslash( $_SERVER['REQUEST_URI'] ) + ); + + $current_status = null; + if ( isset( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ) ) { // WPCS: CSRF ok. + $value = intval( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ); // WPCS: CSRF ok. + if ( in_array( $value, array( self::VALIDATION_ERROR_NEW_STATUS, self::VALIDATION_ERROR_IGNORED_STATUS, self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ), true ) ) { + $current_status = $value; + } + } + + $views['new'] = sprintf( + '%s', + esc_url( + add_query_arg( + self::VALIDATION_ERROR_STATUS_QUERY_VAR, + self::VALIDATION_ERROR_NEW_STATUS, + $current_url + ) + ), + self::VALIDATION_ERROR_NEW_STATUS === $current_status ? 'current' : '', + sprintf( + /* translators: %s is the post count */ + _nx( + 'With New Errors (%s)', + 'With New Errors (%s)', + $with_new_query->found_posts, + 'posts', + 'amp' + ), + number_format_i18n( $with_new_query->found_posts ) + ) + ); + + $views['acknowledged'] = sprintf( + '%s', + esc_url( + add_query_arg( + self::VALIDATION_ERROR_STATUS_QUERY_VAR, + self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS, + $current_url + ) + ), + self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS === $current_status ? 'current' : '', + sprintf( + /* translators: %s is the post count */ + _nx( + 'With Acknowledged Errors (%s)', + 'With Acknowledged Errors (%s)', + $with_acknowledged_query->found_posts, + 'posts', + 'amp' + ), + number_format_i18n( $with_acknowledged_query->found_posts ) + ) + ); + + $views['ignored'] = sprintf( + '%s', + esc_url( + add_query_arg( + self::VALIDATION_ERROR_STATUS_QUERY_VAR, + self::VALIDATION_ERROR_IGNORED_STATUS, + $current_url + ) + ), + self::VALIDATION_ERROR_IGNORED_STATUS === $current_status ? 'current' : '', + sprintf( + /* translators: %s is the post count */ + _nx( + 'With Ignored Errors (%s)', + 'With Ignored Errors (%s)', + $with_ignored_query->found_posts, + 'posts', + 'amp' + ), + number_format_i18n( $with_ignored_query->found_posts ) + ) + ); + + return $views; + } ); + // Add views for filtering validation errors by status. add_filter( 'views_edit-' . self::TAXONOMY_SLUG, function( $views ) { $total_term_count = self::get_validation_error_count(); @@ -2309,11 +2486,14 @@ public static function print_status_meta_box( $post ) { * @return void */ public static function print_validation_errors_meta_box( $post ) { - $errors = array(); + $validation_errors = array(); foreach ( array_filter( explode( "\n", $post->post_content ) ) as $term_slug ) { $term = get_term_by( 'slug', $term_slug, self::TAXONOMY_SLUG ); if ( $term ) { - $errors[] = json_decode( $term->description, true ); + $validation_errors[] = array( + 'term' => $term, + 'data' => json_decode( $term->description, true ), + ); } } @@ -2325,29 +2505,38 @@ public static function print_validation_errors_meta_box( $post ) {
    - +
  • - + + term_group ) : ?> + + term_group ) : ?> + + term_group ) : ?> + + + +
      - +
    • ', $error['parent_name'] ) ); + if ( isset( $error['data']['parent_name'] ) ) { + echo esc_html( sprintf( '<%s …>', $error['data']['parent_name'] ) ); } ?> $value ) { + echo esc_html( sprintf( '<%s', $error['data']['node_name'] ) ); + if ( isset( $error['data']['node_attributes'] ) ) { + foreach ( $error['data']['node_attributes'] as $key => $value ) { printf( ' %s="%s"', esc_html( $key ), esc_html( $value ) ); } } @@ -2362,21 +2551,21 @@ public static function print_validation_errors_meta_box( $post ) { $collasped_details[] = 'parent_name'; ?>
    • - +
    • $value ) { - if ( $key === $error['node_name'] ) { + foreach ( $error['data']['element_attributes'] as $key => $value ) { + if ( $key === $error['data']['node_name'] ) { echo ''; } printf( ' %s="%s"', esc_html( $key ), esc_html( $value ) ); - if ( $key === $error['node_name'] ) { + if ( $key === $error['data']['node_name'] ) { echo ''; } } @@ -2391,8 +2580,8 @@ public static function print_validation_errors_meta_box( $post ) { ?>
    • - - $value ) : ?> + + $value ) : ?>
    • > From 95487df9c5a27b972b61287d841698a8de05f149 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 22 Apr 2018 21:15:04 -0700 Subject: [PATCH 05/55] Update count with Invalid Pages admin menu to use count with new errors --- includes/utils/class-amp-validation-utils.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index 1478e2c3c60..78a9503009f 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -311,13 +311,20 @@ public static function add_admin_menu_validation_status_count() { if ( ! isset( $submenu[ AMP_Options_Manager::OPTION_NAME ] ) ) { return; } - $count = wp_count_posts( self::POST_TYPE_SLUG ); - if ( empty( $count->publish ) ) { + + $query = new WP_Query( array( + 'post_type' => self::POST_TYPE_SLUG, + self::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_NEW_STATUS, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) ); + + if ( 0 === $query->found_posts ) { 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 ) . ''; + $submenu_item[0] .= ' ' . esc_html( number_format_i18n( $query->found_posts ) ) . ''; break; } } From 212171fcc9d6178a713d09848a81544000974009 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 22 Apr 2018 21:20:11 -0700 Subject: [PATCH 06/55] Let non-new validation errors be collapsed by default when viewing invalid URL details --- includes/utils/class-amp-validation-utils.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index 78a9503009f..1a9ecd21a3a 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -2517,7 +2517,7 @@ public static function print_validation_errors_meta_box( $post ) { $collasped_details = array(); ?>
    • -
      +
      term_group ) ? 'open' : ''; ?>> term_group ) : ?> From 65999ea561e012e4f1ea44f46fb031f2330ce415 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 22 Apr 2018 21:27:03 -0700 Subject: [PATCH 07/55] Allow validation errors to be deleted which no longer have any occurances --- includes/utils/class-amp-validation-utils.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index 1a9ecd21a3a..09e0f040850 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -1815,10 +1815,20 @@ public static function register_post_type() { return $content; }, 10, 3 ); + // Prevent user from being able to delete validation errors when they still have associated invalid URLs. + add_filter( 'user_has_cap', function( $allcaps, $caps, $args ) { + if ( isset( $args[0] ) && 'delete_term' === $args[0] && 0 !== get_term( $args[2] )->count ) { + $allcaps = array_merge( + $allcaps, + array_fill_keys( $caps, false ) + ); + } + return $allcaps; + }, 10, 3 ); + // Add row actions. add_filter( 'tag_row_actions', function( $actions, WP_Term $tag ) { if ( self::TAXONOMY_SLUG === $tag->taxonomy ) { - unset( $actions['delete'] ); $term_id = $tag->term_id; if ( self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS !== $tag->term_group ) { $actions[ self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = sprintf( @@ -1889,7 +1899,6 @@ public static function register_post_type() { // Add bulk actions. add_filter( 'bulk_actions-edit-' . self::TAXONOMY_SLUG, function( $bulk_actions ) { - unset( $bulk_actions['delete'] ); $bulk_actions[ self::VALIDATION_ERROR_IGNORE_ACTION ] = __( 'Ignore', 'amp' ); $bulk_actions[ self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = __( 'Acknowledge', 'amp' ); return $bulk_actions; From c482b377613134fb742bbad469c234361ef032ae Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 23 Apr 2018 11:14:58 -0700 Subject: [PATCH 08/55] Ensure checkboxes for validation error terms are always shown; block deletion of non-zero-count terms --- includes/utils/class-amp-validation-utils.php | 93 +++++++++++++++++-- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index 09e0f040850..4307381d64d 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -1277,7 +1277,7 @@ public static function wrapped_callback( $callback ) { self::add_validation_error( array( 'code' => self::ENQUEUED_SCRIPT_CODE, 'handle' => $handle, - 'dependency' => $wp_scripts->registered[ $handle ], + 'dependency' => $wp_scripts->registered[ $handle ], // @todo Remove extra data since too variable. 'sources' => array( $callback['source'], ), @@ -1815,9 +1815,78 @@ public static function register_post_type() { return $content; }, 10, 3 ); + // Hacikly remove amp_validation_error terms before they get bulk deleted (as workaround for WP_Terms_List_Table::column_cb()). + add_action( 'load-edit-tags.php', function() { + $is_delete_tags_request = isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action'] && ! empty( $_REQUEST['delete_tags'] ); // WPCS: CSRF ok. + if ( ! $is_delete_tags_request ) { + return; + } + $requested_delete_tags = array_map( 'intval', (array) $_REQUEST['delete_tags'] ); // WPCS: CSRF ok. + $actual_delete_tags = array(); + $blocked_delete_tags = array(); + foreach ( $requested_delete_tags as $requested_delete_tag ) { + $term = get_term( $requested_delete_tag ); + if ( $term && self::TAXONOMY_SLUG === $term->taxonomy && 0 !== $term->count ) { + $blocked_delete_tags[] = $requested_delete_tag; + } else { + $actual_delete_tags[] = $requested_delete_tag; + } + } + + // Prevent deleting terms that shouldn't be deleted. + $_POST['delete_tags'] = $actual_delete_tags; + $_REQUEST['delete_tags'] = $actual_delete_tags; + + // Show admin notice when terms were blocked from being deleted. + if ( ! empty( $blocked_delete_tags ) ) { + add_filter( 'redirect_term_location', function( $url ) use ( $blocked_delete_tags ) { + return add_query_arg( 'amp_validation_errors_not_deleted', count( $blocked_delete_tags ), $url ); + } ); + } + + // Remove success message if no terms were actually deleted. + if ( empty( $actual_delete_tags ) ) { + add_filter( 'redirect_term_location', function( $url ) { + return remove_query_arg( 'message', $url ); + } ); + } + } ); + + // Show admin notice when validation error terms were skipped from being deleted due to still having associated URLs (workaround for WP_Terms_List_Table::column_cb()). + add_action( 'admin_notices', function() { + if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || empty( $_REQUEST['amp_validation_errors_not_deleted'] ) ) { + return; + } + $count = intval( $_REQUEST['amp_validation_errors_not_deleted'] ); // WPCS: CSRF ok. + printf( + '

      %s

      ', + esc_html( + sprintf( + /* translators: %s is number of validation errors */ + _n( + '%s validation error was not deleted because it still has occurrence on the site.', + '%s validation errors were not deleted because they still have occurrences on the site.', + $count, + 'amp' + ), + number_format_i18n( $count ) + ) + ) + ); + } ); + // Prevent user from being able to delete validation errors when they still have associated invalid URLs. add_filter( 'user_has_cap', function( $allcaps, $caps, $args ) { if ( isset( $args[0] ) && 'delete_term' === $args[0] && 0 !== get_term( $args[2] )->count ) { + /* + * However, only apply this if not on the edit terms screen for validation errors, since + * WP_Terms_List_Table::column_cb() unfortunately has a hard-coded delete_term capability check, so + * without that check passing then the checkbox is not shown. + */ + if ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy && empty( $_REQUEST['action'] ) ) { + return $allcaps; + } + $allcaps = array_merge( $allcaps, array_fill_keys( $caps, false ) @@ -1830,6 +1899,17 @@ public static function register_post_type() { add_filter( 'tag_row_actions', function( $actions, WP_Term $tag ) { if ( self::TAXONOMY_SLUG === $tag->taxonomy ) { $term_id = $tag->term_id; + + /* + * Hide deletion link when there are remaining invalid URLs associated with them. + * Note that this would normally be handled via the user_has_cap filter above, + * but this has to be here due to a problem with WP_Terms_List_Table::column_cb() + * which requires a workaround. + */ + if ( 0 !== $tag->count ) { + unset( $actions['delete'] ); + } + if ( self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS !== $tag->term_group ) { $actions[ self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = sprintf( '%s', @@ -1910,17 +1990,18 @@ public static function register_post_type() { // Prevent query vars from persisting after redirect. add_filter( 'removable_query_args', function( $query_vars ) { $query_vars[] = 'amp_actioned'; - $query_vars[] = 'actioned_count'; + $query_vars[] = 'amp_actioned_count'; + $query_vars[] = 'amp_validation_errors_not_deleted'; return $query_vars; } ); // Show notices for bulk actions. add_action( 'admin_notices', function() { - if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || empty( $_GET['amp_actioned'] ) || empty( $_GET['actioned_count'] ) ) { // WPCS: CSRF ok. + if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || empty( $_GET['amp_actioned'] ) || empty( $_GET['amp_actioned_count'] ) ) { // WPCS: CSRF ok. return; } $actioned = sanitize_key( $_GET['amp_actioned'] ); // WPCS: CSRF ok. - $count = intval( $_GET['actioned_count'] ); // WPCS: CSRF ok. + $count = intval( $_GET['amp_actioned_count'] ); // WPCS: CSRF ok. $message = null; if ( self::VALIDATION_ERROR_IGNORE_ACTION === $actioned ) { $message = sprintf( @@ -2001,8 +2082,8 @@ public static function handle_validation_error_update( $redirect_to, $action, $t } $redirect_to = add_query_arg( array( - 'amp_actioned' => $action, - 'actioned_count' => count( $term_ids ), + 'amp_actioned' => $action, + 'amp_actioned_count' => count( $term_ids ), ), $redirect_to ); From a6d2f7075752ae325fa3926efdf9d756bfd86a82 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 23 Apr 2018 17:42:26 -0700 Subject: [PATCH 09/55] Add sorting of validation errors by creation date --- includes/utils/class-amp-validation-utils.php | 73 +++++++++++++++++-- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index 4307381d64d..735ddc9193a 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -1754,15 +1754,22 @@ public static function register_post_type() { // Override the columns displayed for the validation error terms. add_filter( 'manage_edit-' . self::TAXONOMY_SLUG . '_columns', function( $old_columns ) { return array( - 'cb' => $old_columns['cb'], - 'error' => __( 'Error', 'amp' ), - 'status' => __( 'Status', 'amp' ), - 'details' => __( 'Details', 'amp' ), - 'sources' => __( 'Sources', 'amp' ), - 'posts' => __( 'URLs', 'amp' ), + 'cb' => $old_columns['cb'], + 'error' => __( 'Error', 'amp' ), + 'created_date_gmt' => __( 'Created Date', 'amp' ), + 'status' => __( 'Status', 'amp' ), + 'details' => __( 'Details', 'amp' ), + 'sources' => __( 'Sources', 'amp' ), + 'posts' => __( 'URLs', 'amp' ), ); } ); + // Let the created date column sort by term ID. + add_filter( 'manage_edit-' . self::TAXONOMY_SLUG . '_sortable_columns', function( $sortable_columns ) { + $sortable_columns['created_date_gmt'] = 'term_id'; + return $sortable_columns; + } ); + // Supply the content for the custom columns. add_filter( 'manage_' . self::TAXONOMY_SLUG . '_custom_column', function( $content, $column_name, $term_id ) { $term = get_term( $term_id ); @@ -1793,6 +1800,47 @@ public static function register_post_type() { } else { $content = esc_html__( 'New', 'amp' ); } + break; + case 'created_date_gmt': + $created_datetime = null; + $created_date_gmt = get_term_meta( $term_id, 'created_date_gmt', true ); + if ( $created_date_gmt ) { + try { + $created_datetime = new DateTime( $created_date_gmt, new DateTimeZone( 'UTC' ) ); + $timezone_string = get_option( 'timezone_string' ); + if ( ! $timezone_string && get_option( 'gmt_offset' ) ) { + $timezone_string = timezone_name_from_abbr( '', get_option( 'gmt_offset' ) * HOUR_IN_SECONDS, false ); + } + if ( $timezone_string ) { + $created_datetime->setTimezone( new DateTimeZone( get_option( 'timezone_string' ) ) ); + } + } catch ( Exception $e ) { + unset( $e ); + } + } + if ( ! $created_datetime ) { + $time_ago = __( 'n/a', 'amp' ); + } elseif ( time() - $created_datetime->getTimestamp() < DAY_IN_SECONDS ) { + /* translators: %s is the relative time */ + $time_ago = sprintf( + '%s', + esc_attr( $created_datetime->format( __( 'Y/m/d g:i:s a', 'default' ) ) ), + /* translators: %s is relative time */ + esc_html( sprintf( __( '%s ago', 'default' ), human_time_diff( $created_datetime->getTimestamp() ) ) ) + ); + } else { + $time_ago = mysql2date( __( 'Y/m/d g:i:s a', 'default' ), $created_date_gmt ); + } + + if ( $created_datetime ) { + $time_ago = sprintf( + '', + $created_datetime->format( 'c' ), + $time_ago + ); + } + $content .= $time_ago; + break; case 'details': unset( $validation_error['code'] ); @@ -1936,6 +1984,15 @@ public static function register_post_type() { return $actions; }, 10, 2 ); + // Filter amp_validation_error term query by term group when requested. + add_filter( 'get_terms_defaults', function( $args, $taxonomies ) { + if ( array( self::TAXONOMY_SLUG ) === $taxonomies ) { + $args['orderby'] = 'term_id'; + $args['order'] = 'DESC'; + } + return $args; + }, 10, 2 ); + // Filter amp_validation_error term query by term group when requested. add_action( 'load-edit-tags.php', function() { if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || ! isset( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ) ) { // WPCS: CSRF ok. @@ -2134,7 +2191,9 @@ public static function store_validation_errors( $validation_errors, $url ) { if ( is_wp_error( $r ) ) { continue; } - $term = get_term( $r['term_id'] ); + $term_id = $r['term_id']; + update_term_meta( $term_id, 'created_date_gmt', current_time( 'mysql', true ) ); + $term = get_term( $term_id ); } $terms[ $term_slug ] = $term; } From 546c16360a0a5b993d6208a9c51e93b664278f4c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 23 Apr 2018 18:13:44 -0700 Subject: [PATCH 10/55] Hide irrelevant published status from invalid URL post list Add query vars to removable list --- includes/utils/class-amp-validation-utils.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index 735ddc9193a..09b7476ce9a 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -1469,6 +1469,14 @@ public static function register_post_type() { // Add support for querying posts by amp_validation_error_status. add_filter( 'posts_where', array( __CLASS__, 'filter_posts_where_for_validation_error_status' ), 10, 2 ); + // Hide irrelevant "published" label in the invalid URL post list. + add_filter( 'post_date_column_status', function( $status, $post ) { + if ( self::POST_TYPE_SLUG === get_post_type( $post ) ) { + $status = ''; + } + return $status; + }, 10, 2 ); + // Add recognition of amp_validation_error_status query var (which will only apply in admin since post type is not publicly_queryable). add_filter( 'query_vars', function( $query_vars ) { $query_vars[] = self::VALIDATION_ERROR_STATUS_QUERY_VAR; @@ -2049,6 +2057,8 @@ public static function register_post_type() { $query_vars[] = 'amp_actioned'; $query_vars[] = 'amp_actioned_count'; $query_vars[] = 'amp_validation_errors_not_deleted'; + $query_vars[] = 'amp_remaining_errors'; + $query_vars[] = 'amp_urls_tested'; return $query_vars; } ); From 52f4cddfe914608637f5204cc7df390b349af11a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 23 Apr 2018 22:34:20 -0700 Subject: [PATCH 11/55] Add amp_invalid_url post list table column for error status --- includes/utils/class-amp-validation-utils.php | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index 09b7476ce9a..7286473a9a1 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -2359,6 +2359,7 @@ public static function add_post_columns( $columns ) { $columns = array_merge( $columns, array( + 'error_status' => esc_html__( 'Error Status', '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' ), @@ -2388,17 +2389,54 @@ public static function output_custom_column( $column_name, $post_id ) { return; } + $counts = array_fill_keys( + array( + self::VALIDATION_ERROR_NEW_STATUS, + self::VALIDATION_ERROR_IGNORED_STATUS, + self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS, + ), + 0 + ); + $validation_errors = array(); foreach ( array_filter( explode( "\n", $post->post_content ) ) as $term_slug ) { $term = get_term_by( 'slug', $term_slug, self::TAXONOMY_SLUG ); if ( $term ) { $validation_errors[] = json_decode( $term->description, true ); + if ( isset( $counts[ $term->term_group ] ) ) { + $counts[ $term->term_group ]++; + } } } $errors = self::summarize_validation_errors( $validation_errors ); switch ( $column_name ) { + case 'error_status': + $displayed_counts = array(); + if ( $counts[ self::VALIDATION_ERROR_NEW_STATUS ] ) { + $displayed_counts[] = esc_html( sprintf( + /* translators: %s is count */ + __( 'New: %s', 'amp' ), + number_format_i18n( $counts[ self::VALIDATION_ERROR_NEW_STATUS ] ) + ) ); + } + if ( $counts[ self::VALIDATION_ERROR_IGNORED_STATUS ] ) { + $displayed_counts[] = esc_html( sprintf( + /* translators: %s is count */ + __( 'Ignored: %s', 'amp' ), + number_format_i18n( $counts[ self::VALIDATION_ERROR_IGNORED_STATUS ] ) + ) ); + } + if ( $counts[ self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ] ) { + $displayed_counts[] = esc_html( sprintf( + /* translators: %s is count */ + __( 'Acknowledged: %s', 'amp' ), + number_format_i18n( $counts[ self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ] ) + ) ); + } + echo implode( '
      ', $displayed_counts ); // WPCS: xss ok. + break; case self::REMOVED_ELEMENTS: if ( ! empty( $errors[ self::REMOVED_ELEMENTS ] ) ) { self::output_removed_set( $errors[ self::REMOVED_ELEMENTS ] ); From 8f3147260924d3f5e376281dc59760ecaebf8552 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 23 Apr 2018 23:08:37 -0700 Subject: [PATCH 12/55] Add inline acknowledge/ignore actions to validation errors listed in invalid AMP URL post --- includes/utils/class-amp-validation-utils.php | 57 ++++++++++++++++--- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index 7286473a9a1..297b8985620 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -2035,8 +2035,8 @@ public static function register_post_type() { $referer = wp_get_referer(); $term_id = intval( $_GET['term_id'] ); // WPCS: CSRF ok. $redirect = self::handle_validation_error_update( $referer, $action, array( $term_id ) ); + if ( $redirect !== $referer ) { - $redirect = remove_query_arg( array( 'action', '_wpnonce', 'term_id' ), $redirect ); wp_safe_redirect( $redirect ); exit; } @@ -2062,9 +2062,9 @@ public static function register_post_type() { return $query_vars; } ); - // Show notices for bulk actions. + // Show notices for changes to amp_validation_error terms. add_action( 'admin_notices', function() { - if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || empty( $_GET['amp_actioned'] ) || empty( $_GET['amp_actioned_count'] ) ) { // WPCS: CSRF ok. + if ( ! ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy || self::POST_TYPE_SLUG === get_current_screen()->post_type ) || empty( $_GET['amp_actioned'] ) || empty( $_GET['amp_actioned_count'] ) ) { // WPCS: CSRF ok. return; } $actioned = sanitize_key( $_GET['amp_actioned'] ); // WPCS: CSRF ok. @@ -2703,28 +2703,69 @@ public static function print_validation_errors_meta_box( $post ) { ?>
        term_id; + $edit_terms_url = admin_url( 'edit-tags.php?taxonomy=' . self::TAXONOMY_SLUG ); ?>
      • -
        term_group ) ? 'open' : ''; ?>> +
        term_group ) ? 'open' : ''; ?>> - term_group ) : ?> + term_group ) : ?> - term_group ) : ?> + term_group ) : ?> - term_group ) : ?> + term_group ) : ?> +

        + term_group ) { + $actions[ self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = sprintf( + '%s', + wp_nonce_url( + add_query_arg( + array_merge( array( 'action' => self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ), compact( 'term_id' ) ), + $edit_terms_url + ), + self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION + ), + esc_attr__( 'Acknowledging an error marks it as read. AMP validation errors prevent a URL from being served as AMP.', 'amp' ), + esc_html__( 'Acknowledge', 'amp' ) + ); + } + if ( self::VALIDATION_ERROR_IGNORED_STATUS !== $term->term_group ) { + $actions[ self::VALIDATION_ERROR_IGNORE_ACTION ] = sprintf( + '%s', + wp_nonce_url( + add_query_arg( + array_merge( array( 'action' => self::VALIDATION_ERROR_IGNORE_ACTION ), compact( 'term_id' ) ), + $edit_terms_url + ), + self::VALIDATION_ERROR_IGNORE_ACTION + ), + esc_attr__( 'Ignoring an error prevents it from blocking a URL from being served as AMP.', 'amp' ), + esc_html__( 'Ignore', 'amp' ) + ); + } + echo implode( ' | ', $actions ); // WPCS: xss ok. + ?> +

        • From 1e1516d4342bb2cc4b8502707272ff8e1f55f0b4 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 23 Apr 2018 23:28:33 -0700 Subject: [PATCH 13/55] Remove scheme/host from invalid AMP URLs in list table; show URL as heading on edit post screen --- includes/utils/class-amp-validation-utils.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index 297b8985620..482a0e4da99 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -1431,6 +1431,28 @@ public static function register_post_type() { // Hide the add new post link. $post_type->cap->create_posts = 'do_not_allow'; + // Show URL at the top of the edit form in place of the title (since title support is not present). + add_action( 'edit_form_top', function( $post ) { + if ( self::POST_TYPE_SLUG !== $post->post_type ) { + return; + } + ?> +

          + +

          + array( 'name' => _x( 'AMP Validation Errors', 'taxonomy general name', 'amp' ), From 1cb4a69572d2bfa72ad1009201c65153a206e1dc Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 23 Apr 2018 23:36:59 -0700 Subject: [PATCH 14/55] Add error status to invalid AMP URL publish metabox; add view link --- includes/utils/class-amp-validation-utils.php | 68 ++++++++++++++++--- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index 482a0e4da99..2656e3b9504 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -2411,6 +2411,7 @@ public static function output_custom_column( $column_name, $post_id ) { return; } + // @todo Move into helper function. $counts = array_fill_keys( array( self::VALIDATION_ERROR_NEW_STATUS, @@ -2420,6 +2421,7 @@ public static function output_custom_column( $column_name, $post_id ) { 0 ); + // @todo Move into helper function. $validation_errors = array(); foreach ( array_filter( explode( "\n", $post->post_content ) ) as $term_slug ) { $term = get_term_by( 'slug', $term_slug, self::TAXONOMY_SLUG ); @@ -2682,21 +2684,67 @@ public static function print_status_meta_box( $post ) { $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. + printf( __( 'Last checked: %s', 'amp' ), esc_html( date_i18n( $date_format, strtotime( $post->post_date ) ) ) ); // WPCS: XSS ok. echo '
          '; + + // @todo Move into helper function. + $counts = array_fill_keys( + array( + self::VALIDATION_ERROR_NEW_STATUS, + self::VALIDATION_ERROR_IGNORED_STATUS, + self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS, + ), + 0 + ); + + // @todo Move into helper function. + foreach ( array_filter( explode( "\n", $post->post_content ) ) as $term_slug ) { + $term = get_term_by( 'slug', $term_slug, self::TAXONOMY_SLUG ); + if ( $term && isset( $counts[ $term->term_group ] ) ) { + $counts[ $term->term_group ]++; + } + } + + // @todo De-duplicate with other place where logic is run. + $displayed_counts = array(); + if ( $counts[ self::VALIDATION_ERROR_NEW_STATUS ] ) { + $displayed_counts[] = esc_html( sprintf( + /* translators: %s is count */ + __( 'New: %s', 'amp' ), + number_format_i18n( $counts[ self::VALIDATION_ERROR_NEW_STATUS ] ) + ) ); + } + if ( $counts[ self::VALIDATION_ERROR_IGNORED_STATUS ] ) { + $displayed_counts[] = esc_html( sprintf( + /* translators: %s is count */ + __( 'Ignored: %s', 'amp' ), + number_format_i18n( $counts[ self::VALIDATION_ERROR_IGNORED_STATUS ] ) + ) ); + } + if ( $counts[ self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ] ) { + $displayed_counts[] = esc_html( sprintf( + /* translators: %s is count */ + __( 'Acknowledged: %s', 'amp' ), + number_format_i18n( $counts[ self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ] ) + ) ); + } + + echo '
          '; + echo implode( '
          ', $displayed_counts ); // WPCS: xss ok. + echo '
          '; + printf( '', esc_url( get_delete_post_link( $post->ID ) ), esc_html__( 'Move to Trash', 'default' ) ); + $url = $post->post_title; echo '
          '; + printf( '%s | ', esc_url( $url ), esc_html__( 'View', 'amp' ) ); echo self::get_recheck_link( $post, $redirect_url ); // WPCS: XSS ok. - $url = $post->post_title; - 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. - } + 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 '
      '; From aadadaf4e1c1895421e787fbce13d7bd7afc4b84 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 24 Apr 2018 15:18:41 -0700 Subject: [PATCH 15/55] Only show count of new validation errors that have associated URLs in admin menu Improve column widths for validation errors; hide overflow for details --- includes/utils/class-amp-validation-utils.php | 50 +++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index 2656e3b9504..a4922fcef59 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -1521,14 +1521,25 @@ public static function register_post_type() { // Hide empty term addition form. add_action( 'admin_enqueue_scripts', function() { if ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy ) { - wp_add_inline_style( 'common', '#col-left { display: none; } #col-right { float:none; width: auto; }' ); + wp_add_inline_style( 'common', ' + #col-left { display: none; } + #col-right { float:none; width: auto; } + + /* Improve column widths */ + td.column-details pre, td.column-sources pre { overflow:auto; } + th.column-created_date_gmt { width:15%; } + th.column-status { width:10%; } + ' ); } } ); // Show AMP validation errors under AMP admin menu. add_action( 'admin_menu', function() { $menu_item_label = esc_html__( 'Validation Errors', 'amp' ); - $new_error_count = self::get_validation_error_count( self::VALIDATION_ERROR_NEW_STATUS ); + $new_error_count = self::get_validation_error_count( array( + 'group' => self::VALIDATION_ERROR_NEW_STATUS, + 'ignore_empty' => true, + ) ); if ( $new_error_count ) { $menu_item_label .= ' ' . esc_html( number_format_i18n( $new_error_count ) ) . ''; } @@ -1673,8 +1684,8 @@ public static function register_post_type() { // Add views for filtering validation errors by status. add_filter( 'views_edit-' . self::TAXONOMY_SLUG, function( $views ) { $total_term_count = self::get_validation_error_count(); - $acknowledged_term_count = self::get_validation_error_count( self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ); - $ignored_term_count = self::get_validation_error_count( self::VALIDATION_ERROR_IGNORED_STATUS ); + $acknowledged_term_count = self::get_validation_error_count( array( 'group' => self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ) ); + $ignored_term_count = self::get_validation_error_count( array( 'group' => self::VALIDATION_ERROR_IGNORED_STATUS ) ); $new_term_count = $total_term_count - $acknowledged_term_count - $ignored_term_count; $current_url = remove_query_arg( @@ -2120,29 +2131,40 @@ public static function register_post_type() { printf( '

      %s

      ', esc_html( $message ) ); } } ); - - // @todo Default to hide_empty terms since we don't want to show errors which don't have any instances on the site. } /** * Get the count of validation error terms, optionally restricted by term group (e.g. ignored or acknowledged). * - * @param int|null $group Term group. - * @return int|WP_Error Number of terms in that taxonomy or WP_Error if the taxonomy does not exist. + * @param array $args { + * Args passed into wp_count_terms(). + * + * @type int|null $group Term group. + * @type bool $ignore_empty Ignore terms that are no longer associated with any URLs. Default false. + * } + * @return int Term count. */ - public static function get_validation_error_count( $group = null ) { - $filter = function( $clauses ) use ( $group ) { + public static function get_validation_error_count( $args = array() ) { + $args = array_merge( + array( + 'group' => null, + 'ignore_empty' => false, + ), + $args + ); + + $filter = function( $clauses ) use ( $args ) { global $wpdb; - $clauses['where'] .= $wpdb->prepare( ' AND t.term_group = %d', $group ); + $clauses['where'] .= $wpdb->prepare( ' AND t.term_group = %d', $args['group'] ); return $clauses; }; - if ( isset( $group ) ) { + if ( isset( $args['group'] ) ) { add_filter( 'terms_clauses', $filter ); } self::$should_filter_terms_clauses_for_error_validation_status = false; - $term_count = wp_count_terms( self::TAXONOMY_SLUG ); + $term_count = wp_count_terms( self::TAXONOMY_SLUG, $args ); self::$should_filter_terms_clauses_for_error_validation_status = true; - if ( isset( $group ) ) { + if ( isset( $args['group'] ) ) { remove_filter( 'terms_clauses', $filter ); } return $term_count; From f8852627a5b6410bb361a771da0d364c9f1c9079 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 24 Apr 2018 16:07:51 -0700 Subject: [PATCH 16/55] Remove script dependency object from enqueued_script validation error The before/after inline data is too variable and can cause many duplicate validation errors to accumulate --- includes/utils/class-amp-validation-utils.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index a4922fcef59..6aeb9759861 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -1275,10 +1275,9 @@ public static function wrapped_callback( $callback ) { // Flag all scripts not loaded from the AMP CDN as validation errors. if ( isset( $wp_scripts->registered[ $handle ] ) && 0 !== strpos( $wp_scripts->registered[ $handle ]->src, 'https://cdn.ampproject.org/' ) ) { self::add_validation_error( array( - 'code' => self::ENQUEUED_SCRIPT_CODE, - 'handle' => $handle, - 'dependency' => $wp_scripts->registered[ $handle ], // @todo Remove extra data since too variable. - 'sources' => array( + 'code' => self::ENQUEUED_SCRIPT_CODE, + 'handle' => $handle, + 'sources' => array( $callback['source'], ), ) ); From 7af2db51497c10b1c6afd64f61781fbd775715d8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 24 Apr 2018 17:50:52 -0700 Subject: [PATCH 17/55] Prevent showing AMP validation notice on edit post screen when all validation errors have been ignored * Eliminate duplicated logic parsing post_content to obtain validation errors. * Change "Details" link to "Review Errors" link in edit post screen notice; explain impact of having non-ignored AMP validation errors. Add debug link to notice in Gutenberg. * Add debug_link to amp_validity REST field. * Introduce get_invalid_url_validation_errors method as model getter. --- assets/js/amp-block-validation.js | 14 +- includes/utils/class-amp-validation-utils.php | 189 +++++++++--------- tests/test-class-amp-validation-utils.php | 36 +--- 3 files changed, 115 insertions(+), 124 deletions(-) diff --git a/assets/js/amp-block-validation.js b/assets/js/amp-block-validation.js index 07591f98176..b6c7f724c80 100644 --- a/assets/js/amp-block-validation.js +++ b/assets/js/amp-block-validation.js @@ -172,13 +172,19 @@ var ampBlockValidation = ( function() { ); } - noticeMessage += ' ' + wp.i18n.__( 'Invalid code is stripped when displaying AMP.', 'amp' ); + noticeMessage += ' ' + wp.i18n.__( 'Non-ignored validation errors prevent AMP from being served.', 'amp' ); noticeElement = wp.element.createElement( 'p', {}, [ noticeMessage + ' ', - ampValidity.link && wp.element.createElement( + ampValidity.review_link && wp.element.createElement( 'a', - { key: 'details', href: ampValidity.link, target: '_blank' }, - wp.i18n.__( 'Details', 'amp' ) + { key: 'details', href: ampValidity.review_link, target: '_blank' }, + wp.i18n.__( 'Review issues', 'amp' ) + ), + ampValidity.review_link && ' | ', + ampValidity.debug_link && wp.element.createElement( + 'a', + { key: 'details', href: ampValidity.debug_link, target: '_blank' }, + wp.i18n.__( 'Debug', 'amp' ) ) ] ); diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index 6aeb9759861..17202c0a7c2 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -696,48 +696,49 @@ public static function print_edit_form_validation_status( $post ) { return; } - $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(); + $amp_url = null; + $invalid_url_post = null; + $validation_errors = array(); + if ( is_post_type_viewable( $post->post_type ) ) { + $amp_url = amp_get_permalink( $post->ID ); } // Incorporate frontend validation status if there is a known URL for the post. - $existing_validation_errors = self::get_existing_validation_errors( $post ); - if ( isset( $existing_validation_errors ) ) { - $validation_errors = $existing_validation_errors; - } + $invalid_url_post = self::get_invalid_url_post( $amp_url ); + if ( $invalid_url_post ) { + $validation_errors = wp_list_pluck( + self::get_invalid_url_validation_errors( $invalid_url_post, array( 'ignore_ignored' => true ) ), + 'data' + ); + } elseif ( post_type_supports( $post->post_type, 'editor' ) ) { + // Validate post content outside frontend context. + self::process_markup( $post->post_content ); + $validation_errors = self::$validation_errors; + self::reset_validation_results(); + } if ( empty( $validation_errors ) ) { return; } echo '
      '; 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 ) { + esc_html_e( 'There is content which fails AMP validation. Non-ignored validation errors prevent AMP from being served.', 'amp' ); + if ( $invalid_url_post || $amp_url ) { + if ( $invalid_url_post ) { echo sprintf( ' %s', - esc_url( get_edit_post_link( $validation_status_post ) ), - esc_html__( 'Details', 'amp' ) + esc_url( get_edit_post_link( $invalid_url_post ) ), + esc_html__( 'Review issues', 'amp' ) ); } - if ( $url ) { - if ( $validation_status_post ) { + if ( $amp_url ) { + if ( $invalid_url_post ) { echo ' | '; } echo sprintf( ' %s', - esc_url( self::get_debug_url( $url ) ), + esc_url( self::get_debug_url( $amp_url ) ), esc_attr__( 'Validate URL on frontend but without invalid elements/attributes removed', 'amp' ), esc_html__( 'Debug', 'amp' ) ); @@ -769,29 +770,6 @@ public static function print_edit_form_validation_status( $post ) { echo '

      '; } - /** - * Gets the validation errors for a given post. - * - * These are stored in a custom post type. - * If none exist, returns null. - * - * @param WP_Post $post The post for which to get the validation errors. - * @return array|null $errors The validation errors, if they exist. - */ - public static function get_existing_validation_errors( $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 ) ) { - return $data; - } - } - } - return null; - } - /** * Get source start comment. * @@ -2277,14 +2255,52 @@ public static function store_validation_errors( $validation_errors, $url ) { /** * Gets the existing custom post that stores errors for the $url, if it exists. * - * @todo Rename to get_invalid_url_post(). * @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 ) { + public static function get_invalid_url_post( $url ) { return get_page_by_path( md5( $url ), OBJECT, self::POST_TYPE_SLUG ); } + /** + * Gets validation errors for a given invalid URL post. + * + * @param int|WP_Post $post Post of amp_invalid_url type. + * @param array $args { + * Args. + * + * @type bool $ignore_ignored Exclude validation errors that are ignored. Default false. + * } + * @return array List of errors. + */ + public static function get_invalid_url_validation_errors( $post, $args = array() ) { + $args = array_merge( + array( + 'ignore_ignored' => false, + ), + $args + ); + $post = get_post( $post ); + $errors = array(); + foreach ( array_filter( explode( "\n", $post->post_content ) ) as $term_slug ) { + if ( ! preg_match( '/^[0-9a-f]{32}$/', $term_slug ) ) { + continue; + } + $term = get_term_by( 'slug', $term_slug, self::TAXONOMY_SLUG ); + if ( ! $term ) { + continue; + } + if ( $args['ignore_ignored'] && self::VALIDATION_ERROR_IGNORED_STATUS === $term->term_group ) { + continue; + } + $errors[] = array( + 'term' => $term, + 'data' => json_decode( $term->description, true ), + ); + } + return $errors; + } + /** * Validates the latest published post. * @@ -2443,18 +2459,14 @@ public static function output_custom_column( $column_name, $post_id ) { ); // @todo Move into helper function. - $validation_errors = array(); - foreach ( array_filter( explode( "\n", $post->post_content ) ) as $term_slug ) { - $term = get_term_by( 'slug', $term_slug, self::TAXONOMY_SLUG ); - if ( $term ) { - $validation_errors[] = json_decode( $term->description, true ); - if ( isset( $counts[ $term->term_group ] ) ) { - $counts[ $term->term_group ]++; - } + $validation_errors = self::get_invalid_url_validation_errors( $post_id ); + foreach ( wp_list_pluck( $validation_errors, 'term' ) as $term ) { + if ( isset( $counts[ $term->term_group ] ) ) { + $counts[ $term->term_group ]++; } } - $errors = self::summarize_validation_errors( $validation_errors ); + $error_summary = self::summarize_validation_errors( wp_list_pluck( $validation_errors, 'data' ) ); switch ( $column_name ) { case 'error_status': @@ -2483,23 +2495,23 @@ public static function output_custom_column( $column_name, $post_id ) { echo implode( '
      ', $displayed_counts ); // WPCS: xss ok. break; case self::REMOVED_ELEMENTS: - if ( ! empty( $errors[ self::REMOVED_ELEMENTS ] ) ) { - self::output_removed_set( $errors[ self::REMOVED_ELEMENTS ] ); + if ( ! empty( $error_summary[ self::REMOVED_ELEMENTS ] ) ) { + self::output_removed_set( $error_summary[ 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 ] ); + if ( ! empty( $error_summary[ self::REMOVED_ATTRIBUTES ] ) ) { + self::output_removed_set( $error_summary[ self::REMOVED_ATTRIBUTES ] ); } else { esc_html_e( '--', 'amp' ); } break; case self::SOURCES_INVALID_OUTPUT: - if ( isset( $errors[ self::SOURCES_INVALID_OUTPUT ] ) ) { + if ( isset( $error_summary[ self::SOURCES_INVALID_OUTPUT ] ) ) { $sources = array(); - foreach ( $errors[ self::SOURCES_INVALID_OUTPUT ] as $type => $names ) { + foreach ( $error_summary[ self::SOURCES_INVALID_OUTPUT ] as $type => $names ) { foreach ( array_unique( $names ) as $name ) { $sources[] = sprintf( '%s: %s', esc_html( $type ), esc_html( $name ) ); } @@ -2719,9 +2731,9 @@ public static function print_status_meta_box( $post ) { ); // @todo Move into helper function. - foreach ( array_filter( explode( "\n", $post->post_content ) ) as $term_slug ) { - $term = get_term_by( 'slug', $term_slug, self::TAXONOMY_SLUG ); - if ( $term && isset( $counts[ $term->term_group ] ) ) { + $validation_errors = self::get_invalid_url_validation_errors( $post ); + foreach ( wp_list_pluck( $validation_errors, 'term' ) as $term ) { + if ( isset( $counts[ $term->term_group ] ) ) { $counts[ $term->term_group ]++; } } @@ -2781,17 +2793,7 @@ public static function print_status_meta_box( $post ) { * @return void */ public static function print_validation_errors_meta_box( $post ) { - $validation_errors = array(); - foreach ( array_filter( explode( "\n", $post->post_content ) ) as $term_slug ) { - $term = get_term_by( 'slug', $term_slug, self::TAXONOMY_SLUG ); - if ( $term ) { - $validation_errors[] = array( - 'term' => $term, - 'data' => json_decode( $term->description, true ), - ); - } - } - + $validation_errors = self::get_invalid_url_validation_errors( $post ); ?> #s', $sanitized_html ); $this->assertContains( '', $sanitized_html ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript $this->assertContains( '', $sanitized_html ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript $this->assertContains( '', $sanitized_html ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript @@ -1040,10 +1048,27 @@ public function test_prepare_response() { $this->assertContains( '', $sanitized_html ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript $this->assertContains( '', $sanitized_html ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript + $removed_nodes = array(); + foreach ( AMP_Validation_Utils::$validation_results as $result ) { + if ( $result['sanitized'] && isset( $result['error']['node_name'] ) ) { + $node_name = $result['error']['node_name']; + if ( ! isset( $removed_nodes[ $node_name ] ) ) { + $removed_nodes[ $node_name ] = 0; + } + $removed_nodes[ $node_name ]++; + } + } + $this->assertContains( '', $sanitized_html ); - $this->assertCount( 4, $removed_nodes ); - $this->assertInstanceOf( 'DOMElement', $removed_nodes['script'] ); - $this->assertInstanceOf( 'DOMAttr', $removed_nodes['onclick'] ); + $this->assertCount( 5, AMP_Validation_Utils::$validation_results ); + $this->assertEquals( + array( + 'onclick' => 1, + 'handle' => 3, + 'script' => 1, + ), + $removed_nodes + ); } /** @@ -1052,6 +1077,7 @@ public function test_prepare_response() { * @covers AMP_Theme_Support::prepare_response() */ public function test_prepare_response_bad_html() { + add_filter( 'amp_validation_error_sanitized', '__return_true' ); add_theme_support( 'amp' ); AMP_Theme_Support::init(); @@ -1075,6 +1101,7 @@ public function test_prepare_response_bad_html() { * @covers AMP_Theme_Support::prepare_response() */ public function test_prepare_response_to_add_html5_doctype_and_amp_attribute() { + add_filter( 'amp_validation_error_sanitized', '__return_true' ); add_theme_support( 'amp' ); AMP_Theme_Support::init(); ob_start(); diff --git a/tests/test-class-amp-validation-utils.php b/tests/test-class-amp-validation-utils.php index 3bee3cc2077..fedb125b5da 100644 --- a/tests/test-class-amp-validation-utils.php +++ b/tests/test-class-amp-validation-utils.php @@ -96,6 +96,7 @@ public function tearDown() { $GLOBALS['wp_registered_widgets'] = $this->original_wp_registered_widgets; // WPCS: override ok. remove_theme_support( 'amp' ); unset( $GLOBALS['current_screen'] ); + AMP_Validation_Utils::$locate_sources = false; parent::tearDown(); } @@ -131,7 +132,6 @@ public function test_add_validation_hooks() { AMP_Validation_Utils::add_validation_hooks(); $this->assertEquals( PHP_INT_MAX, has_filter( 'the_content', array( self::TESTED_CLASS, 'decorate_filter_source' ) ) ); $this->assertEquals( PHP_INT_MAX, has_filter( 'the_excerpt', array( self::TESTED_CLASS, 'decorate_filter_source' ) ) ); - $this->assertEquals( 1000, 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' ) ) ); } @@ -248,21 +248,28 @@ public function test_add_block_source_comments( $content, $expected, $query ) { * @covers AMP_Validation_Utils::add_validation_error() */ public function test_track_removed() { - $this->assertEmpty( AMP_Validation_Utils::$validation_errors ); - AMP_Validation_Utils::add_validation_error( array( - 'node' => $this->node, - ) ); + AMP_Validation_Utils::$locate_sources = true; + $this->assertEmpty( AMP_Validation_Utils::$validation_results ); + AMP_Validation_Utils::add_validation_error( + array( + 'node_name' => $this->node->nodeName, + 'code' => AMP_Validation_Utils::INVALID_ELEMENT_CODE, + 'node_attributes' => array(), + ), + array( + 'node' => $this->node, + ) + ); + $this->assertCount( 1, AMP_Validation_Utils::$validation_results ); $this->assertEquals( array( - array( - 'node_name' => 'img', - 'sources' => array(), - 'code' => AMP_Validation_Utils::INVALID_ELEMENT_CODE, - 'node_attributes' => array(), - ), + 'node_name' => 'img', + 'sources' => array(), + 'code' => AMP_Validation_Utils::INVALID_ELEMENT_CODE, + 'node_attributes' => array(), ), - AMP_Validation_Utils::$validation_errors + AMP_Validation_Utils::$validation_results[0]['error'] ); AMP_Validation_Utils::reset_validation_results(); } @@ -273,13 +280,13 @@ public function test_track_removed() { * @covers AMP_Validation_Utils::add_validation_error() */ public function test_was_node_removed() { - $this->assertEmpty( AMP_Validation_Utils::$validation_errors ); + $this->assertEmpty( AMP_Validation_Utils::$validation_results ); AMP_Validation_Utils::add_validation_error( array( 'node' => $this->node, ) ); - $this->assertNotEmpty( AMP_Validation_Utils::$validation_errors ); + $this->assertNotEmpty( AMP_Validation_Utils::$validation_results ); } /** @@ -288,37 +295,39 @@ public function test_was_node_removed() { * @covers AMP_Validation_Utils::process_markup() */ public function test_process_markup() { + add_filter( 'amp_validation_error_sanitized', '__return_true' ); + $this->set_capability(); AMP_Validation_Utils::process_markup( $this->valid_amp_img ); - $this->assertEquals( array(), AMP_Validation_Utils::$validation_errors ); + $this->assertEquals( array(), AMP_Validation_Utils::$validation_results ); AMP_Validation_Utils::reset_validation_results(); $video = '
      elements. - * - * @param WP_Post $post The post for which to output the box. - * @return void - */ - public static function print_validation_errors_meta_box( $post ) { - $validation_errors = self::get_invalid_url_validation_errors( $post ); - ?> - -
      -
        - - term_id; - $edit_terms_url = admin_url( 'edit-tags.php?taxonomy=' . self::TAXONOMY_SLUG ); - ?> -
      • -
        term_group ) ? 'open' : ''; ?>> - - term_group ) : ?> - - term_group ) : ?> - - term_group ) : ?> - - - - -

        - term_group ) { - $actions[ self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = sprintf( - '%s', - wp_nonce_url( - add_query_arg( - array_merge( array( 'action' => self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ), compact( 'term_id' ) ), - $edit_terms_url - ), - self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION - ), - esc_attr__( 'Acknowledging an error marks it as read. AMP validation errors prevent a URL from being served as AMP.', 'amp' ), - esc_html__( 'Acknowledge', 'amp' ) - ); - } - if ( self::VALIDATION_ERROR_IGNORED_STATUS !== $term->term_group ) { - $actions[ self::VALIDATION_ERROR_IGNORE_ACTION ] = sprintf( - '%s', - wp_nonce_url( - add_query_arg( - array_merge( array( 'action' => self::VALIDATION_ERROR_IGNORE_ACTION ), compact( 'term_id' ) ), - $edit_terms_url - ), - self::VALIDATION_ERROR_IGNORE_ACTION - ), - esc_attr__( 'Ignoring an error prevents it from blocking a URL from being served as AMP.', 'amp' ), - esc_html__( 'Ignore', 'amp' ) - ); - } - echo implode( ' | ', $actions ); // WPCS: xss ok. - ?> -

        -
          - -
        • -
          - - - ', $error['data']['parent_name'] ) ); - } - ?> - - $value ) { - printf( ' %s="%s"', esc_html( $key ), esc_html( $value ) ); - } - } - echo esc_html( '>…' ); - ?> - - -
          - -
        • - -
        • -
          - - - $value ) { - if ( $key === $error['data']['node_name'] ) { - echo ''; - } - printf( ' %s="%s"', esc_html( $key ), esc_html( $value ) ); - if ( $key === $error['data']['node_name'] ) { - echo ''; - } - } - echo esc_html( '>' ); - ?> - -
          - -
        • - - - $value ) : ?> -
        • -
          > - -
          - - - -
          - -
          -
          -
        • - -
        -
        -
      • - -
      -
      - '', - self::DEBUG_QUERY_VAR => '', - ), - $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' ) - ); - } - - /** - * Enqueues the block validation script. - * - * @return void - */ - public static function enqueue_block_validation() { - $slug = 'amp-block-validation'; - - wp_enqueue_script( - $slug, - amp_get_asset_url( "js/{$slug}.js" ), - array( 'underscore' ), - AMP__VERSION, - true - ); - - $data = wp_json_encode( array( - 'i18n' => gutenberg_get_jed_locale_data( 'amp' ), // @todo POT file. - 'ampValidityRestField' => self::VALIDITY_REST_FIELD_NAME, - ) ); - wp_add_inline_script( $slug, sprintf( 'ampBlockValidation.boot( %s );', $data ) ); - } - - /** - * Adds fields to the REST API responses, in order to display validation errors. - * - * @return void - */ - public static function add_rest_api_fields() { - if ( amp_is_canonical() ) { - $object_types = get_post_types_by_support( 'editor' ); - } else { - $object_types = array_intersect( - get_post_types_by_support( 'amp' ), - get_post_types( array( - 'show_in_rest' => true, - ) ) - ); - } - - register_rest_field( - $object_types, - self::VALIDITY_REST_FIELD_NAME, - array( - 'get_callback' => array( __CLASS__, 'get_amp_validity_rest_field' ), - 'schema' => array( - 'description' => __( 'AMP validity status', 'amp' ), - 'type' => 'object', - ), - ) - ); - } - - /** - * Adds a field to the REST API responses to display the validation status. - * - * First, get existing errors for the post. - * If there are none, validate the post and return any errors. - * - * @param array $post_data Data for the post. - * @param string $field_name The name of the field to add. - * @param WP_REST_Request $request The name of the field to add. - * @return array|null $validation_data Validation data if it's available, or null. - */ - public static function get_amp_validity_rest_field( $post_data, $field_name, $request ) { - unset( $field_name ); - if ( ! current_user_can( 'edit_post', $post_data['id'] ) ) { - return null; - } - $post = get_post( $post_data['id'] ); - - $validation_status_post = null; - if ( in_array( $request->get_method(), array( 'PUT', 'POST' ), true ) ) { - if ( ! isset( self::$posts_pending_frontend_validation[ $post->ID ] ) ) { - self::$posts_pending_frontend_validation[ $post->ID ] = true; - } - $results = self::validate_queued_posts_on_frontend(); - if ( isset( $results[ $post->ID ] ) && is_int( $results[ $post->ID ] ) ) { - $validation_status_post = get_post( $results[ $post->ID ] ); - } - } - - if ( empty( $validation_status_post ) ) { - // @todo Consider process_markup() if not post type is not viewable and if post type supports editor. - $validation_status_post = self::get_invalid_url_post( amp_get_permalink( $post->ID ) ); - } - - $field = array( - 'errors' => array(), - 'review_link' => null, - 'debug_link' => self::get_debug_url( amp_get_permalink( $post_data['id'] ) ), - ); - - if ( $validation_status_post ) { - $field = array_merge( - $field, - array( - 'review_link' => get_edit_post_link( $validation_status_post->ID, 'raw' ), - 'errors' => wp_list_pluck( - self::get_invalid_url_validation_errors( $validation_status_post, array( 'ignore_ignored' => true ) ), - 'data' - ), - ) - ); - } - - return $field; - } - - /** - * Outputs an admin notice if persistent object cache is not present. - * - * @return void - */ - public static function persistent_object_caching_notice() { - if ( ! wp_using_ext_object_cache() && 'toplevel_page_amp-options' === get_current_screen()->id ) { - printf( - '

      %s %s

      ', - esc_html__( 'The AMP plugin performs at its best when persistent object cache is enabled.', 'amp' ), - esc_url( 'https://codex.wordpress.org/Class_Reference/WP_Object_Cache#Persistent_Caching' ), - esc_html__( 'More details', 'amp' ) - ); - } - } - -} - diff --git a/includes/validation/class-amp-invalid-url-post-type.php b/includes/validation/class-amp-invalid-url-post-type.php new file mode 100644 index 00000000000..13df6d668bc --- /dev/null +++ b/includes/validation/class-amp-invalid-url-post-type.php @@ -0,0 +1,1026 @@ + array( + 'name' => _x( 'Invalid AMP Pages (URLs)', 'post type general name', 'amp' ), + 'menu_name' => __( 'Invalid Pages', 'amp' ), + 'singular_name' => __( 'Invalid AMP Page (URL)', 'amp' ), + 'not_found' => __( 'No invalid AMP pages found', 'amp' ), + 'not_found_in_trash' => __( 'No invalid AMP pages in trash', 'amp' ), + 'search_items' => __( 'Search invalid AMP pages', 'amp' ), + 'edit_item' => __( 'Invalid AMP Page', 'amp' ), + ), + 'supports' => false, + 'public' => false, + 'show_ui' => true, + 'show_in_menu' => AMP_Options_Manager::OPTION_NAME, + // @todo Show in rest. + ) + ); + + // Hide the add new post link. + $post_type->cap->create_posts = 'do_not_allow'; + + if ( is_admin() ) { + add_filter( 'dashboard_glance_items', array( __CLASS__, 'filter_dashboard_glance_items' ) ); + add_action( 'rightnow_end', array( __CLASS__, 'print_dashboard_glance_styles' ) ); + add_action( 'add_meta_boxes', array( __CLASS__, 'add_meta_boxes' ) ); + add_action( 'edit_form_top', array( __CLASS__, 'print_url_as_title' ) ); + add_filter( 'the_title', array( __CLASS__, 'filter_the_title_in_post_list_table' ), 10, 2 ); + + 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' ) ); + } + + // Hide irrelevant "published" label in the invalid URL post list. + add_filter( 'post_date_column_status', function( $status, $post ) { + if ( self::POST_TYPE_SLUG === get_post_type( $post ) ) { + $status = ''; + } + return $status; + }, 10, 2 ); + + // Show AMP validation errors under AMP admin menu. + add_action( 'admin_menu', function() { + $menu_item_label = esc_html__( 'Validation Errors', 'amp' ); + $new_error_count = AMP_Validation_Error_Taxonomy::get_validation_error_count( array( + 'group' => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS, + 'ignore_empty' => true, + ) ); + if ( $new_error_count ) { + $menu_item_label .= ' ' . esc_html( number_format_i18n( $new_error_count ) ) . ''; + } + + add_submenu_page( + AMP_Options_Manager::OPTION_NAME, + esc_html__( 'Validation Errors', 'amp' ), + $menu_item_label, + get_taxonomy( AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG )->cap->manage_terms, // Yes, cap is an object not an array. + // The following esc_attr() is sadly needed due to . + esc_attr( 'edit-tags.php?taxonomy=' . AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG . '&post_type=' . self::POST_TYPE_SLUG ) + ); + } ); + + // Prevent query vars from persisting after redirect. + add_filter( 'removable_query_args', function( $query_vars ) { + $query_vars[] = 'amp_actioned'; + $query_vars[] = self::REMAINING_ERRORS; + $query_vars[] = 'amp_urls_tested'; + return $query_vars; + } ); + } + + /** + * Add count of how many validation error posts there are to the admin menu. + * + * @todo This probably needs to be updated to show the number of amp_invalid_url posts which have validation errors in the new group. + */ + public static function add_admin_menu_validation_status_count() { + global $submenu; + if ( ! isset( $submenu[ AMP_Options_Manager::OPTION_NAME ] ) ) { + return; + } + + $query = new WP_Query( array( + 'post_type' => self::POST_TYPE_SLUG, + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) ); + + if ( 0 === $query->found_posts ) { + 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( number_format_i18n( $query->found_posts ) ) . ''; + break; + } + } + } + + /** + * Gets validation errors for a given invalid URL post. + * + * @param int|WP_Post $post Post of amp_invalid_url type. + * @param array $args { + * Args. + * + * @type bool $ignore_ignored Exclude validation errors that are ignored. Default false. + * } + * @return array List of errors. + */ + public static function get_invalid_url_validation_errors( $post, $args = array() ) { + $args = array_merge( + array( + 'ignore_ignored' => false, + ), + $args + ); + $post = get_post( $post ); + $errors = array(); + + $stored_validation_errors = json_decode( $post->post_content, true ); + if ( ! is_array( $stored_validation_errors ) ) { + return array(); + } + foreach ( $stored_validation_errors as $stored_validation_error ) { + if ( ! isset( $stored_validation_error['term_slug'], $stored_validation_error['sources'] ) ) { + continue; + } + $term = get_term_by( 'slug', $stored_validation_error['term_slug'], AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ); + if ( ! $term ) { + continue; + } + if ( $args['ignore_ignored'] && AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS === $term->term_group ) { + continue; + } + $errors[] = array( + 'term' => $term, + 'data' => array_merge( + json_decode( $term->description, true ), + array( + 'sources' => $stored_validation_error['sources'], + ) + ), + ); + } + return $errors; + } + + /** + * 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_invalid_url_post( $url ) { + return get_page_by_path( md5( $url ), OBJECT, self::POST_TYPE_SLUG ); + } + + /** + * Stores the validation errors. + * + * If there are no validation errors provided, then any existing amp_invalid_url post is deleted. + * + * @todo Rename to validation results? + * @param array $validation_errors Validation errors. + * @param string $url URL on which the validation errors occurred. + * @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. + * @global WP $wp + */ + public static function store_validation_errors( $validation_errors, $url ) { + $post_slug = md5( $url ); + $post = get_page_by_path( $post_slug, OBJECT, self::POST_TYPE_SLUG ); + if ( ! $post ) { + $post = get_page_by_path( $post_slug . '__trashed', OBJECT, self::POST_TYPE_SLUG ); + } + + // 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 + * the validation error. + */ + $stored_validation_errors = array(); + + $terms = array(); + foreach ( $validation_errors as $data ) { + /* + * Exclude sources from data since not available unless sources are being obtained, + * and thus not able to be matched when hashed. + */ + $sources = null; + if ( isset( $data['sources'] ) ) { + $sources = $data['sources']; + unset( $data['sources'] ); + } + ksort( $data ); + $description = wp_json_encode( $data ); + $term_slug = md5( $description ); + + 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( compact( 'description' ) ) ); + 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; + } + $term_id = $r['term_id']; + update_term_meta( $term_id, 'created_date_gmt', current_time( 'mysql', true ) ); + $term = get_term( $term_id ); + } + $terms[ $term_slug ] = $term; + } + + $stored_validation_errors[] = compact( 'term_slug', 'sources' ); + } + + $post_content = wp_json_encode( $stored_validation_errors ); + $placeholder = 'amp_invalid_url_content_placeholder' . wp_rand(); + + // Guard against Kses from corrupting content by adding post_content after content_save_pre filter applies. + $insert_post_content = function( $post_data ) use ( $placeholder, $post_content ) { + $should_supply_post_content = ( + isset( $post_data['post_content'] ) + && + $placeholder === $post_data['post_content'] + && + isset( $post_data['post_type'] ) + && + self::POST_TYPE_SLUG === $post_data['post_type'] + ); + if ( $should_supply_post_content ) { + $post_data['post_content'] = wp_slash( $post_content ); + } + return $post_data; + }; + add_filter( 'wp_insert_post_data', $insert_post_content ); + + // Create a new invalid AMP URL post, or update the existing one. + $r = wp_insert_post( + wp_slash( array( + 'ID' => $post ? $post->ID : null, + 'post_type' => self::POST_TYPE_SLUG, + 'post_title' => $url, + 'post_name' => $post_slug, + 'post_content' => $placeholder, // Content is provided via wp_insert_post_data filter above to guard against Kses-corruption. + 'post_status' => 'publish', // @todo Use draft when doing a post preview? + ) ), + true + ); + remove_filter( 'wp_insert_post_data', $insert_post_content ); + if ( is_wp_error( $r ) ) { + return $r; + } + $post_id = $r; + wp_set_object_terms( $post_id, wp_list_pluck( $terms, 'term_id' ), AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ); + return $post_id; + } + + /** + * 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( + 'error_status' => esc_html__( 'Error Status', 'amp' ), + AMP_Validation_Error_Taxonomy::REMOVED_ELEMENTS => esc_html__( 'Removed Elements', 'amp' ), + AMP_Validation_Error_Taxonomy::REMOVED_ATTRIBUTES => esc_html__( 'Removed Attributes', 'amp' ), + AMP_Validation_Error_Taxonomy::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; + } + + // @todo Move into helper function. + $counts = array_fill_keys( + array( + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS, + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS, + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS, + ), + 0 + ); + + // @todo Move into helper function. + $validation_errors = self::get_invalid_url_validation_errors( $post_id ); + foreach ( wp_list_pluck( $validation_errors, 'term' ) as $term ) { + if ( isset( $counts[ $term->term_group ] ) ) { + $counts[ $term->term_group ]++; + } + } + + $error_summary = AMP_Validation_Error_Taxonomy::summarize_validation_errors( wp_list_pluck( $validation_errors, 'data' ) ); + + switch ( $column_name ) { + case 'error_status': + $displayed_counts = array(); + if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS ] ) { + $displayed_counts[] = esc_html( sprintf( + /* translators: %s is count */ + __( 'New: %s', 'amp' ), + number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS ] ) + ) ); + } + if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS ] ) { + $displayed_counts[] = esc_html( sprintf( + /* translators: %s is count */ + __( 'Ignored: %s', 'amp' ), + number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS ] ) + ) ); + } + if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ] ) { + $displayed_counts[] = esc_html( sprintf( + /* translators: %s is count */ + __( 'Acknowledged: %s', 'amp' ), + number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ] ) + ) ); + } + echo implode( '
      ', $displayed_counts ); // WPCS: xss ok. + break; + case AMP_Validation_Error_Taxonomy::REMOVED_ELEMENTS: + if ( ! empty( $error_summary[ AMP_Validation_Error_Taxonomy::REMOVED_ELEMENTS ] ) ) { + $items = array(); + foreach ( $error_summary[ AMP_Validation_Error_Taxonomy::REMOVED_ELEMENTS ] 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. + } else { + esc_html_e( '--', 'amp' ); + } + break; + case AMP_Validation_Error_Taxonomy::REMOVED_ATTRIBUTES: + if ( ! empty( $error_summary[ AMP_Validation_Error_Taxonomy::REMOVED_ATTRIBUTES ] ) ) { + $items = array(); + foreach ( $error_summary[ AMP_Validation_Error_Taxonomy::REMOVED_ATTRIBUTES ] 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. + } else { + esc_html_e( '--', 'amp' ); + } + break; + case AMP_Validation_Error_Taxonomy::SOURCES_INVALID_OUTPUT: + if ( isset( $error_summary[ AMP_Validation_Error_Taxonomy::SOURCES_INVALID_OUTPUT ] ) ) { + $sources = array(); + foreach ( $error_summary[ AMP_Validation_Error_Taxonomy::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 = $post->post_title; + + if ( ! empty( $url ) ) { + $actions[ self::RECHECK_ACTION ] = self::get_recheck_link( $post, get_edit_post_link( $post->ID, 'raw' ), $url ); + + $actions[ AMP_Validation_Manager::DEBUG_QUERY_VAR ] = sprintf( + '%s', + esc_url( AMP_Validation_Manager::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 ) { + $post = get_post( $item ); + if ( empty( $post ) ) { + continue; + } + $url = $post->post_title; + if ( empty( $url ) ) { + continue; + } + + $validation_errors = AMP_Validation_Manager::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( + '

      %s

      ', + 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 ); + $post = get_post( $post_id ); + $url = $post->post_title; + if ( isset( $_GET['recheck_url'] ) ) { + $url = wp_validate_redirect( wp_unslash( $_GET['recheck_url'] ) ); + } + $validation_errors = AMP_Validation_Manager::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( __( 'Last checked: %s', 'amp' ), esc_html( date_i18n( $date_format, strtotime( $post->post_date ) ) ) ); // WPCS: XSS ok. + echo '
      '; + + // @todo Move into helper function. + $counts = array_fill_keys( + array( + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS, + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS, + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS, + ), + 0 + ); + + // @todo Move into helper function. + $validation_errors = self::get_invalid_url_validation_errors( $post ); + foreach ( wp_list_pluck( $validation_errors, 'term' ) as $term ) { + if ( isset( $counts[ $term->term_group ] ) ) { + $counts[ $term->term_group ]++; + } + } + + // @todo De-duplicate with other place where logic is run. + $displayed_counts = array(); + if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS ] ) { + $displayed_counts[] = esc_html( sprintf( + /* translators: %s is count */ + __( 'New: %s', 'amp' ), + number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS ] ) + ) ); + } + if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS ] ) { + $displayed_counts[] = esc_html( sprintf( + /* translators: %s is count */ + __( 'Ignored: %s', 'amp' ), + number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS ] ) + ) ); + } + if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ] ) { + $displayed_counts[] = esc_html( sprintf( + /* translators: %s is count */ + __( 'Acknowledged: %s', 'amp' ), + number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ] ) + ) ); + } + + echo '
      '; + echo implode( '
      ', $displayed_counts ); // WPCS: xss ok. + echo '
      '; + + printf( '', esc_url( get_delete_post_link( $post->ID ) ), esc_html__( 'Move to Trash', 'default' ) ); + $url = $post->post_title; + + echo '
      '; + printf( '%s | ', esc_url( $url ), esc_html__( 'View', 'amp' ) ); + echo self::get_recheck_link( $post, $redirect_url ); // WPCS: XSS ok. + printf( + ' | %s', + esc_url( AMP_Validation_Manager::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 ) { + $validation_errors = self::get_invalid_url_validation_errors( $post ); + ?> + +
      +
        + + term_id; + $edit_terms_url = admin_url( 'edit-tags.php?taxonomy=' . AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ); + ?> +
      • +
        term_group ) ? 'open' : ''; ?>> + + term_group ) : ?> + + term_group ) : ?> + + term_group ) : ?> + + + + +

        + term_group ) { + $actions[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = sprintf( + '%s', + wp_nonce_url( + add_query_arg( + array_merge( array( 'action' => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ), compact( 'term_id' ) ), + $edit_terms_url + ), + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGE_ACTION + ), + esc_attr__( 'Acknowledging an error marks it as read. AMP validation errors prevent a URL from being served as AMP.', 'amp' ), + esc_html__( 'Acknowledge', 'amp' ) + ); + } + if ( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS !== $term->term_group ) { + $actions[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORE_ACTION ] = sprintf( + '%s', + wp_nonce_url( + add_query_arg( + array_merge( array( 'action' => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORE_ACTION ), compact( 'term_id' ) ), + $edit_terms_url + ), + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORE_ACTION + ), + esc_attr__( 'Ignoring an error prevents it from blocking a URL from being served as AMP.', 'amp' ), + esc_html__( 'Ignore', 'amp' ) + ); + } + echo implode( ' | ', $actions ); // WPCS: xss ok. + ?> +

        +
          + +
        • +
          + + + ', $error['data']['parent_name'] ) ); + } + ?> + + $value ) { + printf( ' %s="%s"', esc_html( $key ), esc_html( $value ) ); + } + } + echo esc_html( '>…' ); + ?> + + +
          + +
        • + +
        • +
          + + + $value ) { + if ( $key === $error['data']['node_name'] ) { + echo ''; + } + printf( ' %s="%s"', esc_html( $key ), esc_html( $value ) ); + if ( $key === $error['data']['node_name'] ) { + echo ''; + } + } + echo esc_html( '>' ); + ?> + +
          + +
        • + + + $value ) : ?> +
        • +
          > + +
          + + + +
          + +
          +
          +
        • + +
        +
        +
      • + +
      +
      + post_type ) { + return; + } + ?> +

      + +

      + post_type === self::POST_TYPE_SLUG && self::POST_TYPE_SLUG === get_post_type( $post ) ) { + $title = preg_replace( '#^(\w+:)?//[^/]+#', '', $title ); + } + return $title; + } + + /** + * 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' ) + ); + } + + /** + * Filter At a Glance items add AMP Validation Errors. + * + * @param array $items At a glance items. + * @return array Items. + */ + public static function filter_dashboard_glance_items( $items ) { + + $query = new WP_Query( array( + 'post_type' => self::POST_TYPE_SLUG, + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_NEW_STATUS, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) ); + + if ( 0 !== $query->found_posts ) { + $items[] = sprintf( + '%s', + esc_url( admin_url( + add_query_arg( + array( + 'post_type' => self::POST_TYPE_SLUG, + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_NEW_STATUS, + ), + 'edit.php' + ) + ) ), + esc_html( sprintf( + /* translators: %s is the validation error count */ + _n( + '%s URL w/ new AMP errors', + '%s URLs w/ new AMP errors', + $query->found_posts, + 'amp' + ), + $query->found_posts + ) ) + ); + } + return $items; + } + + /** + * Print styles for the At a Glance widget. + */ + public static function print_dashboard_glance_styles() { + ?> + + array( + 'name' => _x( 'AMP Validation Errors', 'taxonomy general name', 'amp' ), + 'singular_name' => _x( 'AMP Validation Error', 'taxonomy singular name', 'amp' ), + 'search_items' => __( 'Search AMP Validation Errors', 'amp' ), + 'all_items' => __( 'All AMP Validation Errors', 'amp' ), + 'edit_item' => __( 'Edit AMP Validation Error', 'amp' ), + 'update_item' => __( 'Update AMP Validation Error', 'amp' ), + 'menu_name' => __( 'Validation Errors', 'amp' ), + 'back_to_items' => __( 'Back to AMP Validation Errors', 'amp' ), + 'popular_items' => __( 'Frequent Validation Errors', 'amp' ), + 'view_item' => __( 'View Validation Error', 'amp' ), + 'add_new_item' => __( 'Add New Validation Error', 'amp' ), // Makes no sense. + 'new_item_name' => __( 'New Validation Error Hash', 'amp' ), // Makes no sense. + 'not_found' => __( 'No validation errors found.', 'amp' ), + 'no_terms' => __( 'Validation Error', 'amp' ), + 'items_list_navigation' => __( 'Validation errors navigation', 'amp' ), + 'items_list' => __( 'Validation errors list', 'amp' ), + /* translators: Tab heading when selecting from the most used terms */ + 'most_used' => __( 'Most Used Validation Errors', 'amp' ), + ), + 'public' => false, + 'show_ui' => true, // @todo False because we need a custom UI. + 'show_tagcloud' => false, + 'show_in_quick_edit' => false, + 'hierarchical' => false, // Or true? Code could be the parent term? + 'show_in_menu' => true, + 'meta_box_cb' => false, // See print_validation_errors_meta_box(). + 'capabilities' => array( + 'assign_terms' => 'do_not_allow', + 'edit_terms' => 'do_not_allow', + // Note that delete_terms is needed so the checkbox (cb) table column will work. + ), + ) ); + + // Include searching taxonomy term descriptions and sources term meta. + add_filter( 'terms_clauses', function( $clauses, $taxonomies, $args ) { + global $wpdb; + if ( ! empty( $args['search'] ) && in_array( self::TAXONOMY_SLUG, $taxonomies, true ) ) { + $clauses['where'] = preg_replace( + '#(?<=\()(?=\(t\.name LIKE \')#', + $wpdb->prepare( '(tt.description LIKE %s) OR ', '%' . $wpdb->esc_like( $args['search'] ) . '%' ), + $clauses['where'] + ); + } + return $clauses; + }, 10, 3 ); + + // Hide empty term addition form. + add_action( 'admin_enqueue_scripts', function() { + if ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy ) { + wp_add_inline_style( 'common', ' + #col-left { display: none; } + #col-right { float:none; width: auto; } + + /* Improve column widths */ + td.column-details pre, td.column-sources pre { overflow:auto; } + th.column-created_date_gmt { width:15%; } + th.column-status { width:10%; } + ' ); + } + } ); + + // Make sure parent menu item is expanded when visiting the taxonomy term page. + add_filter( 'parent_file', function( $parent_file ) { + if ( get_current_screen()->taxonomy === self::TAXONOMY_SLUG ) { + $parent_file = AMP_Options_Manager::OPTION_NAME; + } + return $parent_file; + }, 10, 2 ); + + // Replace the primary column to be error instead of the removed name column.. + add_filter( 'list_table_primary_column', function( $primary_column ) { + if ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy ) { + $primary_column = 'error'; + } + return $primary_column; + } ); + + add_filter( 'posts_where', array( __CLASS__, 'filter_posts_where_for_validation_error_status' ), 10, 2 ); + + add_filter( 'views_edit-' . self::TAXONOMY_SLUG, array( __CLASS__, 'filter_views_edit' ) ); + + // Override the columns displayed for the validation error terms. + add_filter( 'manage_edit-' . self::TAXONOMY_SLUG . '_columns', function( $old_columns ) { + return array( + 'cb' => $old_columns['cb'], + 'error' => __( 'Error', 'amp' ), + 'created_date_gmt' => __( 'Created Date', 'amp' ), + 'status' => __( 'Status', 'amp' ), + 'details' => __( 'Details', 'amp' ), + 'posts' => __( 'URLs', 'amp' ), + ); + } ); + + // Let the created date column sort by term ID. + add_filter( 'manage_edit-' . self::TAXONOMY_SLUG . '_sortable_columns', function( $sortable_columns ) { + $sortable_columns['created_date_gmt'] = 'term_id'; + return $sortable_columns; + } ); + + add_filter( 'manage_' . self::TAXONOMY_SLUG . '_custom_column', array( __CLASS__, 'filter_manage_custom_columns' ), 10, 3 ); + add_action( 'load-edit-tags.php', array( __CLASS__, 'prevent_bulk_deleting_non_empty_terms' ) ); + add_action( 'admin_notices', array( __CLASS__, 'show_bulk_delete_blocked_error_notice' ) ); + + // Prevent user from being able to delete validation errors when they still have associated invalid URLs. + add_filter( 'user_has_cap', function( $allcaps, $caps, $args ) { + if ( isset( $args[0] ) && 'delete_term' === $args[0] && 0 !== get_term( $args[2] )->count ) { + /* + * However, only apply this if not on the edit terms screen for validation errors, since + * WP_Terms_List_Table::column_cb() unfortunately has a hard-coded delete_term capability check, so + * without that check passing then the checkbox is not shown. + */ + if ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy && empty( $_REQUEST['action'] ) ) { + return $allcaps; + } + + $allcaps = array_merge( + $allcaps, + array_fill_keys( $caps, false ) + ); + } + return $allcaps; + }, 10, 3 ); + + // Add row actions. + add_filter( 'tag_row_actions', function( $actions, WP_Term $tag ) { + if ( self::TAXONOMY_SLUG === $tag->taxonomy ) { + $term_id = $tag->term_id; + + /* + * Hide deletion link when there are remaining invalid URLs associated with them. + * Note that this would normally be handled via the user_has_cap filter above, + * but this has to be here due to a problem with WP_Terms_List_Table::column_cb() + * which requires a workaround. + */ + if ( 0 !== $tag->count ) { + unset( $actions['delete'] ); + } + + if ( self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS !== $tag->term_group ) { + $actions[ self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = sprintf( + '%s', + wp_nonce_url( + add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ), compact( 'term_id' ) ) ), + self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION + ), + esc_attr__( 'Acknowledging an error marks it as read. AMP validation errors prevent a URL from being served as AMP.', 'amp' ), + esc_html__( 'Acknowledge', 'amp' ) + ); + } + if ( self::VALIDATION_ERROR_IGNORED_STATUS !== $tag->term_group ) { + $actions[ self::VALIDATION_ERROR_IGNORE_ACTION ] = sprintf( + '%s', + wp_nonce_url( + add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_IGNORE_ACTION ), compact( 'term_id' ) ) ), + self::VALIDATION_ERROR_IGNORE_ACTION + ), + esc_attr__( 'Ignoring an error prevents it from blocking a URL from being served as AMP.', 'amp' ), + esc_html__( 'Ignore', 'amp' ) + ); + } + } + return $actions; + }, 10, 2 ); + + // Filter amp_validation_error term query by term group when requested. + add_filter( 'get_terms_defaults', function( $args, $taxonomies ) { + if ( array( AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ) === $taxonomies ) { + $args['orderby'] = 'term_id'; + $args['order'] = 'DESC'; + } + return $args; + }, 10, 2 ); + + // Filter amp_validation_error term query by term group when requested. + add_action( 'load-edit-tags.php', function() { + if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || ! isset( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ) ) { // WPCS: CSRF ok. + return; + } + self::$should_filter_terms_clauses_for_error_validation_status = true; + $group = intval( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ); // WPCS: CSRF ok. + if ( ! in_array( $group, array( self::VALIDATION_ERROR_NEW_STATUS, self::VALIDATION_ERROR_IGNORED_STATUS, self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ), true ) ) { + return; + } + add_filter( 'terms_clauses', function( $clauses, $taxonomies ) use ( $group ) { + global $wpdb; + if ( self::TAXONOMY_SLUG === $taxonomies[0] && self::$should_filter_terms_clauses_for_error_validation_status ) { + $clauses['where'] .= $wpdb->prepare( ' AND t.term_group = %d', $group ); + } + return $clauses; + }, 10, 2 ); + } ); + + // Handle inline edit links. + add_action( 'load-edit-tags.php', function() { + if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || ! isset( $_GET['action'] ) || ! isset( $_GET['_wpnonce'] ) || ! isset( $_GET['term_id'] ) ) { // WPCS: CSRF ok. + return; + } + $action = sanitize_key( $_GET['action'] ); // WPCS: CSRF ok. + check_admin_referer( $action ); + $tax = get_taxonomy( self::TAXONOMY_SLUG ); + if ( ! current_user_can( $tax->cap->manage_terms ) ) { // Yes it is an object. + return; + } + + $referer = wp_get_referer(); + $term_id = intval( $_GET['term_id'] ); // WPCS: CSRF ok. + $redirect = self::handle_validation_error_update( $referer, $action, array( $term_id ) ); + + if ( $redirect !== $referer ) { + wp_safe_redirect( $redirect ); + exit; + } + } ); + + // Add bulk actions. + add_filter( 'bulk_actions-edit-' . self::TAXONOMY_SLUG, function( $bulk_actions ) { + $bulk_actions[ self::VALIDATION_ERROR_IGNORE_ACTION ] = __( 'Ignore', 'amp' ); + $bulk_actions[ self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = __( 'Acknowledge', 'amp' ); + return $bulk_actions; + } ); + + // Handle bulk actions. + add_filter( 'handle_bulk_actions-edit-' . self::TAXONOMY_SLUG, array( __CLASS__, 'handle_validation_error_update' ), 10, 3 ); + + // Prevent query vars from persisting after redirect. + add_filter( 'removable_query_args', function( $query_vars ) { + $query_vars[] = 'amp_actioned'; + $query_vars[] = 'amp_actioned_count'; + $query_vars[] = 'amp_validation_errors_not_deleted'; + return $query_vars; + } ); + + // Show notices for changes to amp_validation_error terms. + add_action( 'admin_notices', function() { + if ( ! ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy || AMP_Invalid_URL_Post_Type::POST_TYPE_SLUG === get_current_screen()->post_type ) || empty( $_GET['amp_actioned'] ) || empty( $_GET['amp_actioned_count'] ) ) { // WPCS: CSRF ok. + return; + } + $actioned = sanitize_key( $_GET['amp_actioned'] ); // WPCS: CSRF ok. + $count = intval( $_GET['amp_actioned_count'] ); // WPCS: CSRF ok. + $message = null; + if ( self::VALIDATION_ERROR_IGNORE_ACTION === $actioned ) { + $message = sprintf( + /* translators: %s is number of errors ignored */ + _n( + 'Ignored %s error. It will no longer block related URLs from being served as AMP.', + 'Ignored %s errors. They will no longer block related URLs from being served as AMP.', + number_format_i18n( $count ), + 'amp' + ), + $count + ); + } elseif ( self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION === $actioned ) { + $message = sprintf( + /* translators: %s is number of errors acknowledged */ + _n( + 'Acknowledged %s error. It will continue to block related URLs from being served as AMP.', + 'Acknowledged %s errors. They will continue to block related URLs from being served as AMP.', + number_format_i18n( $count ), + 'amp' + ), + $count + ); + } + + if ( $message ) { + printf( '

      %s

      ', esc_html( $message ) ); + } + } ); + + // Add recognition of amp_validation_error_status query var (which will only apply in admin since post type is not publicly_queryable). + add_filter( 'query_vars', function( $query_vars ) { + $query_vars[] = self::VALIDATION_ERROR_STATUS_QUERY_VAR; + return $query_vars; + } ); + + // Add views for filtering validation errors by status. + add_filter( 'views_edit-' . AMP_Invalid_URL_Post_Type::POST_TYPE_SLUG, function( $views ) { + unset( $views['publish'] ); + + $args = array( + 'post_type' => AMP_Invalid_URL_Post_Type::POST_TYPE_SLUG, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ); + + $with_new_query = new WP_Query( array_merge( + $args, + array( self::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_NEW_STATUS ) + ) ); + $with_acknowledged_query = new WP_Query( array_merge( + $args, + array( self::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ) + ) ); + $with_ignored_query = new WP_Query( array_merge( + $args, + array( self::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_IGNORED_STATUS ) + ) ); + + $current_url = remove_query_arg( + array_merge( + wp_removable_query_args(), + array( 's' ) // For some reason behavior of posts list table is to not persist the search query. + ), + wp_unslash( $_SERVER['REQUEST_URI'] ) + ); + + $current_status = null; + if ( isset( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ) ) { // WPCS: CSRF ok. + $value = intval( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ); // WPCS: CSRF ok. + if ( in_array( $value, array( self::VALIDATION_ERROR_NEW_STATUS, self::VALIDATION_ERROR_IGNORED_STATUS, self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ), true ) ) { + $current_status = $value; + } + } + + $views['new'] = sprintf( + '%s', + esc_url( + add_query_arg( + self::VALIDATION_ERROR_STATUS_QUERY_VAR, + self::VALIDATION_ERROR_NEW_STATUS, + $current_url + ) + ), + self::VALIDATION_ERROR_NEW_STATUS === $current_status ? 'current' : '', + sprintf( + /* translators: %s is the post count */ + _nx( + 'With New Errors (%s)', + 'With New Errors (%s)', + $with_new_query->found_posts, + 'posts', + 'amp' + ), + number_format_i18n( $with_new_query->found_posts ) + ) + ); + + $views['acknowledged'] = sprintf( + '%s', + esc_url( + add_query_arg( + self::VALIDATION_ERROR_STATUS_QUERY_VAR, + self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS, + $current_url + ) + ), + self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS === $current_status ? 'current' : '', + sprintf( + /* translators: %s is the post count */ + _nx( + 'With Acknowledged Errors (%s)', + 'With Acknowledged Errors (%s)', + $with_acknowledged_query->found_posts, + 'posts', + 'amp' + ), + number_format_i18n( $with_acknowledged_query->found_posts ) + ) + ); + + $views['ignored'] = sprintf( + '%s', + esc_url( + add_query_arg( + self::VALIDATION_ERROR_STATUS_QUERY_VAR, + self::VALIDATION_ERROR_IGNORED_STATUS, + $current_url + ) + ), + self::VALIDATION_ERROR_IGNORED_STATUS === $current_status ? 'current' : '', + sprintf( + /* translators: %s is the post count */ + _nx( + 'With Ignored Errors (%s)', + 'With Ignored Errors (%s)', + $with_ignored_query->found_posts, + 'posts', + 'amp' + ), + number_format_i18n( $with_ignored_query->found_posts ) + ) + ); + + return $views; + } ); + } + + /** + * Get the count of validation error terms, optionally restricted by term group (e.g. ignored or acknowledged). + * + * @param array $args { + * Args passed into wp_count_terms(). + * + * @type int|null $group Term group. + * @type bool $ignore_empty Ignore terms that are no longer associated with any URLs. Default false. + * } + * @return int Term count. + */ + public static function get_validation_error_count( $args = array() ) { + $args = array_merge( + array( + 'group' => null, + 'ignore_empty' => false, + ), + $args + ); + + $filter = function( $clauses ) use ( $args ) { + global $wpdb; + $clauses['where'] .= $wpdb->prepare( ' AND t.term_group = %d', $args['group'] ); + return $clauses; + }; + if ( isset( $args['group'] ) ) { + add_filter( 'terms_clauses', $filter ); + } + self::$should_filter_terms_clauses_for_error_validation_status = false; + $term_count = wp_count_terms( self::TAXONOMY_SLUG, $args ); + self::$should_filter_terms_clauses_for_error_validation_status = true; + if ( isset( $args['group'] ) ) { + remove_filter( 'terms_clauses', $filter ); + } + return $term_count; + } + + /** + * Add support for querying posts by amp_validation_error_status. + * + * Add recognition of amp_validation_error_status query var for amp_invalid_url post queries. + * + * @see WP_Tax_Query::get_sql_for_clause() + * + * @param string $where SQL WHERE clause. + * @param WP_Query $query Query. + * @return string Modified WHERE clause. + */ + public static function filter_posts_where_for_validation_error_status( $where, WP_Query $query ) { + global $wpdb; + if ( + in_array( AMP_Invalid_URL_Post_Type::POST_TYPE_SLUG, (array) $query->get( 'post_type' ), true ) + && + is_numeric( $query->get( self::VALIDATION_ERROR_STATUS_QUERY_VAR ) ) + ) { + $where .= $wpdb->prepare( + " AND ( + SELECT 1 + FROM $wpdb->term_relationships + INNER JOIN $wpdb->term_taxonomy ON $wpdb->term_taxonomy.term_taxonomy_id = $wpdb->term_relationships.term_taxonomy_id + INNER JOIN $wpdb->terms ON $wpdb->terms.term_id = $wpdb->term_taxonomy.term_id + WHERE + $wpdb->term_taxonomy.taxonomy = %s + AND + $wpdb->term_relationships.object_id = $wpdb->posts.ID + AND + $wpdb->terms.term_group = %d + LIMIT 1 + )", + self::TAXONOMY_SLUG, + $query->get( self::VALIDATION_ERROR_STATUS_QUERY_VAR ) + ); + } + return $where; + } + + /** + * Gets the AMP validation response. + * + * Returns the current validation errors the sanitizers found in rendering the page. + * + * @param array $validation_errors Validation errors. + * @return array The AMP validity of the markup. + */ + public static function summarize_validation_errors( $validation_errors ) { + $results = array(); + $removed_elements = array(); + $removed_attributes = array(); + $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; + } + $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'] ); + + if ( isset( $source['type'], $source['name'] ) ) { + $invalid_sources[ $source['type'] ][] = $source['name']; + } + } + } + + $results = array_merge( + array( + self::SOURCES_INVALID_OUTPUT => $invalid_sources, + ), + compact( + 'removed_elements', + 'removed_attributes' + ), + $results + ); + + return $results; + } + + /** + * Add views for filtering validation errors by status. + * + * @param array $views Views. + * @return array Views. + */ + public static function filter_views_edit( $views ) { + $total_term_count = self::get_validation_error_count(); + $acknowledged_term_count = self::get_validation_error_count( array( 'group' => self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ) ); + $ignored_term_count = self::get_validation_error_count( array( 'group' => self::VALIDATION_ERROR_IGNORED_STATUS ) ); + $new_term_count = $total_term_count - $acknowledged_term_count - $ignored_term_count; + + $current_url = remove_query_arg( + array_merge( + wp_removable_query_args(), + array( 's' ) // For some reason behavior of posts list table is to not persist the search query. + ), + wp_unslash( $_SERVER['REQUEST_URI'] ) + ); + + $current_status = null; + if ( isset( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ) ) { // WPCS: CSRF ok. + $value = intval( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ); // WPCS: CSRF ok. + if ( in_array( $value, array( self::VALIDATION_ERROR_NEW_STATUS, self::VALIDATION_ERROR_IGNORED_STATUS, self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ), true ) ) { + $current_status = $value; + } + } + + $views['all'] = sprintf( + '%s', + esc_url( remove_query_arg( self::VALIDATION_ERROR_STATUS_QUERY_VAR, $current_url ) ), + null === $current_status ? 'current' : '', + sprintf( + /* translators: %s is the term count */ + _nx( + 'All (%s)', + 'All (%s)', + $total_term_count, + 'terms', + 'amp' + ), + number_format_i18n( $total_term_count ) + ) + ); + + $views['new'] = sprintf( + '%s', + esc_url( + add_query_arg( + self::VALIDATION_ERROR_STATUS_QUERY_VAR, + self::VALIDATION_ERROR_NEW_STATUS, + $current_url + ) + ), + self::VALIDATION_ERROR_NEW_STATUS === $current_status ? 'current' : '', + sprintf( + /* translators: %s is the term count */ + _nx( + 'New (%s)', + 'New (%s)', + $new_term_count, + 'terms', + 'amp' + ), + number_format_i18n( $new_term_count ) + ) + ); + + $views['acknowledged'] = sprintf( + '%s', + esc_url( + add_query_arg( + self::VALIDATION_ERROR_STATUS_QUERY_VAR, + self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS, + $current_url + ) + ), + self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS === $current_status ? 'current' : '', + sprintf( + /* translators: %s is the term count */ + _nx( + 'Acknowledged (%s)', + 'Acknowledged (%s)', + $acknowledged_term_count, + 'terms', + 'amp' + ), + number_format_i18n( $acknowledged_term_count ) + ) + ); + + $views['ignored'] = sprintf( + '%s', + esc_url( + add_query_arg( + self::VALIDATION_ERROR_STATUS_QUERY_VAR, + self::VALIDATION_ERROR_IGNORED_STATUS, + $current_url + ) + ), + self::VALIDATION_ERROR_IGNORED_STATUS === $current_status ? 'current' : '', + sprintf( + /* translators: %s is the term count */ + _nx( + 'Ignored (%s)', + 'Ignored (%s)', + $ignored_term_count, + 'terms', + 'amp' + ), + number_format_i18n( $ignored_term_count ) + ) + ); + return $views; + } + + /** + * Supply the content for the custom columns. + * + * @param string $content Column content. + * @param string $column_name Column name. + * @param int $term_id Term ID. + * @return string Content. + */ + public static function filter_manage_custom_columns( $content, $column_name, $term_id ) { + $term = get_term( $term_id ); + + $validation_error = json_decode( $term->description, true ); + if ( ! isset( $validation_error['code'] ) ) { + $validation_error['code'] = 'unknown'; + } + + switch ( $column_name ) { + case 'error': + $content .= '

      '; + $content .= sprintf( '%s', esc_html( $validation_error['code'] ) ); + if ( 'invalid_element' === $validation_error['code'] || 'invalid_attribute' === $validation_error['code'] ) { + $content .= sprintf( ': %s', esc_html( $validation_error['node_name'] ) ); + } + $content .= '

      '; + + if ( isset( $validation_error['message'] ) ) { + $content .= sprintf( '

      %s

      ', esc_html( $validation_error['message'] ) ); + } + break; + case 'status': + if ( self::VALIDATION_ERROR_IGNORED_STATUS === $term->term_group ) { + $content = esc_html__( 'Ignored', 'amp' ); + } elseif ( self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS === $term->term_group ) { + $content = esc_html__( 'Acknowledged', 'amp' ); + } else { + $content = esc_html__( 'New', 'amp' ); + } + break; + case 'created_date_gmt': + $created_datetime = null; + $created_date_gmt = get_term_meta( $term_id, 'created_date_gmt', true ); + if ( $created_date_gmt ) { + try { + $created_datetime = new DateTime( $created_date_gmt, new DateTimeZone( 'UTC' ) ); + $timezone_string = get_option( 'timezone_string' ); + if ( ! $timezone_string && get_option( 'gmt_offset' ) ) { + $timezone_string = timezone_name_from_abbr( '', get_option( 'gmt_offset' ) * HOUR_IN_SECONDS, false ); + } + if ( $timezone_string ) { + $created_datetime->setTimezone( new DateTimeZone( get_option( 'timezone_string' ) ) ); + } + } catch ( Exception $e ) { + unset( $e ); + } + } + if ( ! $created_datetime ) { + $time_ago = __( 'n/a', 'amp' ); + } elseif ( time() - $created_datetime->getTimestamp() < DAY_IN_SECONDS ) { + /* translators: %s is the relative time */ + $time_ago = sprintf( + '%s', + esc_attr( $created_datetime->format( __( 'Y/m/d g:i:s a', 'default' ) ) ), + /* translators: %s is relative time */ + esc_html( sprintf( __( '%s ago', 'default' ), human_time_diff( $created_datetime->getTimestamp() ) ) ) + ); + } else { + $time_ago = mysql2date( __( 'Y/m/d g:i:s a', 'default' ), $created_date_gmt ); + } + + if ( $created_datetime ) { + $time_ago = sprintf( + '', + $created_datetime->format( 'c' ), + $time_ago + ); + } + $content .= $time_ago; + + break; + case 'details': + unset( $validation_error['code'] ); + unset( $validation_error['message'] ); + $content = sprintf( '
      %s
      ', esc_html( wp_json_encode( $validation_error, 128 /* JSON_PRETTY_PRINT */ | 64 /* JSON_UNESCAPED_SLASHES */ ) ) ); + break; + } + return $content; + } + + /** + * Hacikly remove amp_validation_error terms before they get bulk deleted (as workaround for WP_Terms_List_Table::column_cb()). + */ + public static function prevent_bulk_deleting_non_empty_terms() { + $is_delete_tags_request = isset( $_REQUEST['action'] ) && 'delete' === $_REQUEST['action'] && ! empty( $_REQUEST['delete_tags'] ); // WPCS: CSRF ok. + if ( ! $is_delete_tags_request ) { + return; + } + $requested_delete_tags = array_map( 'intval', (array) $_REQUEST['delete_tags'] ); // WPCS: CSRF ok. + $actual_delete_tags = array(); + $blocked_delete_tags = array(); + foreach ( $requested_delete_tags as $requested_delete_tag ) { + $term = get_term( $requested_delete_tag ); + if ( $term && self::TAXONOMY_SLUG === $term->taxonomy && 0 !== $term->count ) { + $blocked_delete_tags[] = $requested_delete_tag; + } else { + $actual_delete_tags[] = $requested_delete_tag; + } + } + + // Prevent deleting terms that shouldn't be deleted. + $_POST['delete_tags'] = $actual_delete_tags; + $_REQUEST['delete_tags'] = $actual_delete_tags; + + // Show admin notice when terms were blocked from being deleted. + if ( ! empty( $blocked_delete_tags ) ) { + add_filter( 'redirect_term_location', function( $url ) use ( $blocked_delete_tags ) { + return add_query_arg( 'amp_validation_errors_not_deleted', count( $blocked_delete_tags ), $url ); + } ); + } + + // Remove success message if no terms were actually deleted. + if ( empty( $actual_delete_tags ) ) { + add_filter( 'redirect_term_location', function( $url ) { + return remove_query_arg( 'message', $url ); + } ); + } + } + + /** + * Show admin notice when validation error terms were skipped from being deleted due to still having associated URLs (workaround for WP_Terms_List_Table::column_cb()). + */ + public static function show_bulk_delete_blocked_error_notice() { + if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || empty( $_REQUEST['amp_validation_errors_not_deleted'] ) ) { // WPCS: CSRF OK. + return; + } + $count = intval( $_REQUEST['amp_validation_errors_not_deleted'] ); // WPCS: CSRF ok. + printf( + '

      %s

      ', + esc_html( + sprintf( + /* translators: %s is number of validation errors */ + _n( + '%s validation error was not deleted because it still has occurrence on the site.', + '%s validation errors were not deleted because they still have occurrences on the site.', + $count, + 'amp' + ), + number_format_i18n( $count ) + ) + ) + ); + } + + /** + * Handle bulk and inline edits to amp_validation_error terms. + * + * @param string $redirect_to Redirect to. + * @param string $action Action. + * @param int[] $term_ids Term IDs. + * + * @return string Redirect. + */ + public static function handle_validation_error_update( $redirect_to, $action, $term_ids ) { + $term_group = null; + if ( self::VALIDATION_ERROR_IGNORE_ACTION === $action ) { + $term_group = self::VALIDATION_ERROR_IGNORED_STATUS; + } elseif ( self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION === $action ) { + $term_group = self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS; + } + + if ( $term_group ) { + $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 ); + } + foreach ( $term_ids as $term_id ) { + wp_update_term( $term_id, self::TAXONOMY_SLUG, compact( 'term_group' ) ); + } + if ( false !== $has_pre_term_description_filter ) { + add_filter( 'pre_term_description', 'wp_filter_kses', $has_pre_term_description_filter ); + } + $redirect_to = add_query_arg( + array( + 'amp_actioned' => $action, + 'amp_actioned_count' => count( $term_ids ), + ), + $redirect_to + ); + } + + return $redirect_to; + } + +} diff --git a/includes/validation/class-amp-validation-manager.php b/includes/validation/class-amp-validation-manager.php new file mode 100644 index 00000000000..46f486f03fd --- /dev/null +++ b/includes/validation/class-amp-validation-manager.php @@ -0,0 +1,1422 @@ + false, + 'locate_sources' => false, + ), + $args + ); + + self::$debug = $args['debug']; + self::$locate_sources = $args['locate_sources']; + + add_action( 'init', array( 'AMP_Invalid_URL_Post_Type', 'register' ) ); + add_action( 'init', array( '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' ) ); + + add_action( 'edit_form_top', array( __CLASS__, 'print_edit_form_validation_status' ), 10, 2 ); + add_action( 'all_admin_notices', array( __CLASS__, 'plugin_notice' ) ); + + add_action( 'rest_api_init', array( __CLASS__, 'add_rest_api_fields' ) ); + + // 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. + } + } ); + + if ( self::$locate_sources ) { + self::add_validation_hooks(); + } + } + + /** + * Add hooks for doing validation during preprocessing/sanitizing. + * + * @todo Rename to add_validation_error_source_tracing(). + */ + public static function add_validation_hooks() { + self::wrap_widget_callbacks(); + + add_action( 'all', array( __CLASS__, 'wrap_hook_callbacks' ) ); + $wrapped_filters = array( 'the_content', 'the_excerpt' ); + foreach ( $wrapped_filters as $wrapped_filter ) { + add_filter( $wrapped_filter, array( __CLASS__, 'decorate_filter_source' ), PHP_INT_MAX ); + } + + add_filter( 'do_shortcode_tag', array( __CLASS__, 'decorate_shortcode_source' ), -1, 2 ); + + $do_blocks_priority = has_filter( 'the_content', 'do_blocks' ); + $is_gutenberg_active = ( + false !== $do_blocks_priority + && + class_exists( 'WP_Block_Type_Registry' ) + ); + if ( $is_gutenberg_active ) { + add_filter( 'the_content', array( __CLASS__, 'add_block_source_comments' ), $do_blocks_priority - 1 ); + } + } + + /** + * Handle save_post action to queue re-validation of the post on the frontend. + * + * @see AMP_Validation_Manager::validate_queued_posts_on_frontend() + * + * @param int $post_id Post ID. + * @param WP_Post $post Post. + */ + public static function handle_save_post_prompting_validation( $post_id, $post ) { + $should_validate_post = ( + is_post_type_viewable( $post->post_type ) + && + ! wp_is_post_autosave( $post ) + && + ! wp_is_post_revision( $post ) + && + ! isset( self::$posts_pending_frontend_validation[ $post_id ] ) + ); + if ( $should_validate_post ) { + self::$posts_pending_frontend_validation[ $post_id ] = true; + + // 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_Manager::handle_save_post_prompting_validation() + * + * @return array Mapping of post ID to the result of validating or storing the validation result. + */ + public static function validate_queued_posts_on_frontend() { + $posts = array_filter( + array_map( 'get_post', array_keys( array_filter( self::$posts_pending_frontend_validation ) ) ), + function( $post ) { + return $post && post_supports_amp( $post ) && 'trash' !== $post->post_status; + } + ); + + $validation_posts = array(); + + // @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 ) { + $validation_posts[ $post->ID ] = new WP_Error( 'no_amp_permalink' ); + continue; + } + + // Prevent re-validating. + self::$posts_pending_frontend_validation[ $post->ID ] = false; + + $validation_errors = self::validate_url( $url ); + if ( is_wp_error( $validation_errors ) ) { + $validation_posts[ $post->ID ] = $validation_errors; + } else { + $validation_posts[ $post->ID ] = AMP_Invalid_URL_Post_Type::store_validation_errors( $validation_errors, $url ); + } + } + + return $validation_posts; + } + + /** + * Adds fields to the REST API responses, in order to display validation errors. + * + * @return void + */ + public static function add_rest_api_fields() { + if ( amp_is_canonical() ) { + $object_types = get_post_types_by_support( 'editor' ); + } else { + $object_types = array_intersect( + get_post_types_by_support( 'amp' ), + get_post_types( array( + 'show_in_rest' => true, + ) ) + ); + } + + register_rest_field( + $object_types, + self::VALIDITY_REST_FIELD_NAME, + array( + 'get_callback' => array( __CLASS__, 'get_amp_validity_rest_field' ), + 'schema' => array( + 'description' => __( 'AMP validity status', 'amp' ), + 'type' => 'object', + ), + ) + ); + } + + /** + * Adds a field to the REST API responses to display the validation status. + * + * First, get existing errors for the post. + * If there are none, validate the post and return any errors. + * + * @param array $post_data Data for the post. + * @param string $field_name The name of the field to add. + * @param WP_REST_Request $request The name of the field to add. + * @return array|null $validation_data Validation data if it's available, or null. + */ + public static function get_amp_validity_rest_field( $post_data, $field_name, $request ) { + unset( $field_name ); + if ( ! current_user_can( 'edit_post', $post_data['id'] ) ) { + return null; + } + $post = get_post( $post_data['id'] ); + + $validation_status_post = null; + if ( in_array( $request->get_method(), array( 'PUT', 'POST' ), true ) ) { + if ( ! isset( self::$posts_pending_frontend_validation[ $post->ID ] ) ) { + self::$posts_pending_frontend_validation[ $post->ID ] = true; + } + $results = self::validate_queued_posts_on_frontend(); + if ( isset( $results[ $post->ID ] ) && is_int( $results[ $post->ID ] ) ) { + $validation_status_post = get_post( $results[ $post->ID ] ); + } + } + + if ( empty( $validation_status_post ) ) { + // @todo Consider process_markup() if not post type is not viewable and if post type supports editor. + $validation_status_post = AMP_Invalid_URL_Post_Type::get_invalid_url_post( amp_get_permalink( $post->ID ) ); + } + + $field = array( + 'errors' => array(), + 'review_link' => null, + 'debug_link' => self::get_debug_url( amp_get_permalink( $post_data['id'] ) ), + ); + + if ( $validation_status_post ) { + $field = array_merge( + $field, + array( + 'review_link' => get_edit_post_link( $validation_status_post->ID, 'raw' ), + 'errors' => wp_list_pluck( + AMP_Invalid_URL_Post_Type::get_invalid_url_validation_errors( $validation_status_post, array( 'ignore_ignored' => true ) ), + 'data' + ), + ) + ); + } + + return $field; + } + + /** + * Processes markup, to determine AMP validity. + * + * Passes $markup through the AMP sanitizers. + * Also passes a 'validation_error_callback' to keep track of stripped attributes and nodes. + * + * @todo Eliminate since unused. + * + * @param string $markup The markup to process. + * @return string Sanitized markup. + */ + public static function process_markup( $markup ) { + AMP_Theme_Support::register_content_embed_handlers(); + + /** 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, + 'validation_error_callback' => 'AMP_Validation_Manager::add_validation_error', + ); + + $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. + * + * @return boolean $has_cap Whether the current user has the capability. + */ + public static function has_cap() { + return current_user_can( 'edit_posts' ); + } + + /** + * Add validation error. + * + * @param array $error { + * Data. + * + * @type string $code Error code. + * @type DOMElement|DOMNode $node The removed node. + * } + * @param array $data Additional data, including the node. + * + * @return bool Whether the validation error should result in sanitization. + */ + public static function add_validation_error( array $error, array $data = array() ) { + $node = null; + $matches = null; + $sources = null; + + if ( isset( $data['node'] ) && $data['node'] instanceof DOMNode ) { + $node = $data['node']; + } + + if ( self::$locate_sources ) { + if ( ! empty( $error['sources'] ) ) { + $sources = $error['sources']; + } elseif ( $node ) { + $sources = self::locate_sources( $node ); + } + } + unset( $error['sources'] ); + + if ( ! isset( $error['code'] ) ) { + $error['code'] = 'unknown'; + } + + /** + * Filters the validation error array. + * + * This allows plugins to add amend additional properties which can help with + * more accurately identifying a validation error beyond the name of the parent + * node and the element's attributes. 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. If looking to force a + * validation error to be ignored, use the 'amp_validation_error_sanitized' + * filter instead of attempting to return an empty value with this filter (as + * that is not supported). + * + * @since 1.0 + * + * @param array $error Validation error to be printed. + * @param array $context { + * Context data for validation error sanitization. + * + * @type DOMNode $node Node for which the validation error is being reported. May be null. + * } + */ + $error = apply_filters( 'amp_validation_error', $error, compact( 'node' ) ); + + // @todo Move this into a helper function. + ksort( $error ); + $slug = md5( wp_json_encode( $error ) ); + $term = get_term_by( 'slug', $slug, AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ); + + if ( ! self::$debug && ! empty( $term ) && AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS === $term->term_group ) { + $sanitized = true; + } else { + $sanitized = false; + } + + /** + * Filters whether the validation error should be sanitized. + * + * 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. + * + * @since 1.0 + * + * @param bool $sanitized Whether sanitized. + * @param array $context { + * Context data for validation error sanitization. + * + * @type array $error Validation error being sanitized. + * } + */ + $sanitized = apply_filters( 'amp_validation_error_sanitized', $sanitized, compact( 'error' ) ); + + // Add sources back into the $error for referencing later. @todo It may be cleaner to store sources separately to avoid having to re-remove later during storage. + $error = array_merge( $error, compact( 'sources' ) ); + + self::$validation_results[] = compact( 'error', 'sanitized' ); + return $sanitized; + } + + /** + * Reset the stored removed nodes and attributes. + * + * After testing if the markup is valid, + * these static values will remain. + * So reset them in case another test is needed. + * + * @return void + */ + public static function reset_validation_results() { + self::$validation_results = 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. + * + * @param WP_Post $post The updated post. + * @return void + */ + public static function print_edit_form_validation_status( $post ) { + if ( ! post_supports_amp( $post ) || ! self::has_cap() ) { + return; + } + + // Skip if the post type is not viewable on the frontend, since we need a permalink to validate. + if ( ! is_post_type_viewable( $post->post_type ) ) { + return; + } + + $amp_url = amp_get_permalink( $post->ID ); + $invalid_url_post = AMP_Invalid_URL_Post_Type::get_invalid_url_post( $amp_url ); + if ( ! $invalid_url_post ) { + return; + } + + $validation_errors = wp_list_pluck( + AMP_Invalid_URL_Post_Type::get_invalid_url_validation_errors( $invalid_url_post, array( 'ignore_ignored' => true ) ), + 'data' + ); + + // No validation errors so abort. + if ( empty( $validation_errors ) ) { + return; + } + + echo '
      '; + echo '

      '; + esc_html_e( 'There is content which fails AMP validation. Non-ignored validation errors prevent AMP from being served.', 'amp' ); + echo sprintf( + ' %s', + esc_url( get_edit_post_link( $invalid_url_post ) ), + esc_html__( 'Review issues', 'amp' ) + ); + echo ' | '; + echo sprintf( + ' %s', + esc_url( self::get_debug_url( $amp_url ) ), + esc_attr__( 'Validate URL on frontend but without invalid elements/attributes removed', 'amp' ), + esc_html__( 'Debug', 'amp' ) + ); + echo '

      '; + + $results = AMP_Validation_Error_Taxonomy::summarize_validation_errors( array_unique( $validation_errors, SORT_REGULAR ) ); + $removed_sets = array(); + if ( ! empty( $results[ AMP_Validation_Error_Taxonomy::REMOVED_ELEMENTS ] ) && is_array( $results[ AMP_Validation_Error_Taxonomy::REMOVED_ELEMENTS ] ) ) { + $removed_sets[] = array( + 'label' => __( 'Invalid elements:', 'amp' ), + 'names' => array_map( 'sanitize_key', $results[ AMP_Validation_Error_Taxonomy::REMOVED_ELEMENTS ] ), + ); + } + if ( ! empty( $results[ AMP_Validation_Error_Taxonomy::REMOVED_ATTRIBUTES ] ) && is_array( $results[ AMP_Validation_Error_Taxonomy::REMOVED_ATTRIBUTES ] ) ) { + $removed_sets[] = array( + 'label' => __( 'Invalid attributes:', 'amp' ), + 'names' => array_map( 'sanitize_key', $results[ AMP_Validation_Error_Taxonomy::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'] ) ); + self::output_removed_set( $removed_set['names'] ); + echo '

      '; + } + + echo '
      '; + } + + /** + * Get source start comment. + * + * @param array $source Source data. + * @param bool $is_start Whether the comment is the start or end. + * @return string HTML Comment. + */ + public static function get_source_comment( array $source, $is_start = true ) { + unset( $source['reflection'] ); + return sprintf( + '', + $is_start ? '' : '/', + str_replace( '--', '', wp_json_encode( $source ) ) + ); + } + + /** + * Parse source comment. + * + * @param DOMComment $comment Comment. + * @return array|null Parsed source or null if not a source comment. + */ + public static function parse_source_comment( DOMComment $comment ) { + if ( ! preg_match( '#^\s*(?P/)?amp-source-stack\s+(?P{.+})\s*$#s', $comment->nodeValue, $matches ) ) { + return null; + } + + $source = json_decode( $matches['args'], true ); + $closing = ! empty( $matches['closing'] ); + + return compact( 'source', 'closing' ); + } + + /** + * 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()[ starts-with( ., "amp-source-stack" ) or starts-with( ., "/amp-source-stack" ) ]', $node ); + $sources = array(); + $matches = array(); + + foreach ( $comments as $comment ) { + $parsed_comment = self::parse_source_comment( $comment ); + if ( ! $parsed_comment ) { + continue; + } + if ( $parsed_comment['closing'] ) { + array_pop( $sources ); + } else { + $sources[] = $parsed_comment['source']; + } + } + + $is_enqueued_link = ( + $node instanceof DOMElement + && + 'link' === $node->nodeName + && + preg_match( '/(?P.+)-css$/', (string) $node->getAttribute( 'id' ), $matches ) + && + isset( self::$enqueued_style_sources[ $matches['handle'] ] ) + ); + if ( $is_enqueued_link ) { + $sources = array_merge( + self::$enqueued_style_sources[ $matches['handle'] ], + $sources + ); + } + + /** + * Script dependency. + * + * @var _WP_Dependency $script_dependency + */ + if ( $node instanceof DOMElement && 'script' === $node->nodeName ) { + $enqueued_script_handles = array_intersect( wp_scripts()->done, array_keys( self::$enqueued_script_sources ) ); + + if ( $node->hasAttribute( 'src' ) ) { + + // External script. + $src = $node->getAttribute( 'src' ); + foreach ( $enqueued_script_handles as $enqueued_script_handle ) { + $script_dependency = wp_scripts()->registered[ $enqueued_script_handle ]; + $is_matching_script = ( + $script_dependency + && + $script_dependency->src + && + // Script attribute is haystack because includes protocol and may include query args (like ver). + false !== strpos( $src, preg_replace( '#^https?:(?=//)#', '', $script_dependency->src ) ) + ); + if ( $is_matching_script ) { + $sources = array_merge( + self::$enqueued_script_sources[ $enqueued_script_handle ], + $sources + ); + break; + } + } + } elseif ( $node->firstChild ) { + + // Inline script. + $text = $node->textContent; + foreach ( $enqueued_script_handles as $enqueued_script_handle ) { + $inline_scripts = array_filter( array_merge( + (array) wp_scripts()->get_data( $enqueued_script_handle, 'data' ), + (array) wp_scripts()->get_data( $enqueued_script_handle, 'before' ), + (array) wp_scripts()->get_data( $enqueued_script_handle, 'after' ) + ) ); + foreach ( $inline_scripts as $inline_script ) { + /* + * Check to see if the inline script is inside (or the same) as the script in the document. + * Note that WordPress takes the registered inline script and will output it with newlines + * padding it, and sometimes with the script wrapped by CDATA blocks. + */ + if ( false !== strpos( $text, trim( $inline_script ) ) ) { + $sources = array_merge( + self::$enqueued_script_sources[ $enqueued_script_handle ], + $sources + ); + break; + } + } + } + } + } + + 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()[ starts-with( ., "amp-source-stack" ) or starts-with( ., "/amp-source-stack" ) ]' ) as $comment ) { + if ( self::parse_source_comment( $comment ) ) { + $comments[] = $comment; + } + } + foreach ( $comments as $comment ) { + $comment->parentNode->removeChild( $comment ); + } + } + + /** + * Add block source comments. + * + * @param string $content Content prior to blocks being processed. + * @return string Content with source comments added. + */ + public static function add_block_source_comments( $content ) { + self::$block_content_index = 0; + + $start_block_pattern = implode( '', array( + '##s', + ) ); + + return preg_replace_callback( + $start_block_pattern, + array( __CLASS__, 'handle_block_source_comment_replacement' ), + $content + ); + } + + /** + * Handle block source comment replacement. + * + * @see \AMP_Validation_Manager::add_block_source_comments() + * + * @param array $matches Matches. + * + * @return string Replaced. + */ + protected static function handle_block_source_comment_replacement( $matches ) { + $replaced = $matches[0]; + + // Obtain source information for block. + $source = array( + 'block_name' => $matches['name'], + 'post_id' => get_the_ID(), + ); + + if ( empty( $matches['closing'] ) ) { + $source['block_content_index'] = self::$block_content_index; + self::$block_content_index++; + } + + // Make implicit core namespace explicit. + $is_implicit_core_namespace = ( false === strpos( $source['block_name'], '/' ) ); + $source['block_name'] = $is_implicit_core_namespace ? 'core/' . $source['block_name'] : $source['block_name']; + + if ( ! empty( $matches['attributes'] ) ) { + $source['block_attrs'] = json_decode( $matches['attributes'] ); + } + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $source['block_name'] ); + if ( $block_type && $block_type->is_dynamic() ) { + $callback_source = self::get_source( $block_type->render_callback ); + if ( $callback_source ) { + $source = array_merge( + $source, + $callback_source + ); + } + } + + if ( ! empty( $matches['closing'] ) ) { + $replaced .= self::get_source_comment( $source, false ); + } else { + $replaced = self::get_source_comment( $source, true ) . $replaced; + if ( ! empty( $matches['self_closing'] ) ) { + unset( $source['block_content_index'] ); + $replaced .= self::get_source_comment( $source, false ); + } + } + return $replaced; + } + + /** + * Wrap callbacks for registered widgets to keep track of queued assets and the source for anything printed for validation. + * + * @global array $wp_filter + * @return void + */ + public static function wrap_widget_callbacks() { + global $wp_registered_widgets; + foreach ( $wp_registered_widgets as $widget_id => &$registered_widget ) { + $source = self::get_source( $registered_widget['callback'] ); + if ( ! $source ) { + continue; + } + $source['widget_id'] = $widget_id; + + $function = $registered_widget['callback']; + $accepted_args = 2; // For the $instance and $args arguments. + $callback = compact( 'function', 'accepted_args', 'source' ); + + $registered_widget['callback'] = self::wrapped_callback( $callback ); + } + } + + /** + * Wrap filter/action callback functions for a given hook. + * + * Wrapped callback functions are reset to their original functions after invocation. + * This runs at the 'all' action. The shutdown hook is excluded. + * + * @global WP_Hook[] $wp_filter + * @param string $hook Hook name for action or filter. + * @return void + */ + public static function wrap_hook_callbacks( $hook ) { + global $wp_filter; + + if ( ! isset( $wp_filter[ $hook ] ) || 'shutdown' === $hook ) { + return; + } + + self::$current_hook_source_stack[ $hook ] = array(); + foreach ( $wp_filter[ $hook ]->callbacks as $priority => &$callbacks ) { + foreach ( $callbacks as &$callback ) { + $source = self::get_source( $callback['function'] ); + if ( ! $source ) { + continue; + } + + $reflection = $source['reflection']; + unset( $source['reflection'] ); // Omit from stored source. + + // Add hook to stack for decorate_filter_source to read from. + self::$current_hook_source_stack[ $hook ][] = $source; + + /* + * A current limitation with wrapping callbacks is that the wrapped function cannot have + * any parameters passed by reference. Without this the result is: + * + * > PHP Warning: Parameter 1 to wp_default_styles() expected to be a reference, value given. + */ + if ( self::has_parameters_passed_by_reference( $reflection ) ) { + continue; + } + + $source['hook'] = $hook; + $original_function = $callback['function']; + $wrapped_callback = self::wrapped_callback( array_merge( + $callback, + compact( 'priority', 'source', 'hook' ) + ) ); + + $callback['function'] = function() use ( &$callback, $wrapped_callback, $original_function ) { + $callback['function'] = $original_function; // Restore original. + return call_user_func_array( $wrapped_callback, func_get_args() ); + }; + } + } + } + + /** + * Determine whether the given reflection method/function has params passed by reference. + * + * @since 0.7 + * @param ReflectionFunction|ReflectionMethod $reflection Reflection. + * @return bool Whether there are parameters passed by reference. + */ + protected static function has_parameters_passed_by_reference( $reflection ) { + foreach ( $reflection->getParameters() as $parameter ) { + if ( $parameter->isPassedByReference() ) { + return true; + } + } + return false; + } + + /** + * 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; + } + $source['shortcode'] = $tag; + + $output = implode( '', array( + self::get_source_comment( $source, true ), + $output, + self::get_source_comment( $source, false ), + ) ); + return $output; + } + + /** + * Wraps output of a filter to add source stack comments. + * + * @todo Duplicate with AMP_Validation_Manager::wrap_buffer_with_source_comments()? + * @param string $value Value. + * @return string Value wrapped in source comments. + */ + public static function decorate_filter_source( $value ) { + + // Abort if the output is not a string and it doesn't contain any HTML tags. + if ( ! is_string( $value ) || ! preg_match( '/<.+?>/s', $value ) ) { + return $value; + } + + $post = get_post(); + $source = array( + 'hook' => current_filter(), + 'filter' => true, + ); + if ( $post ) { + $source['post_id'] = $post->ID; // @todo This is causing duplicate validation errors to occur when only variance is post_id. + $source['post_type'] = $post->post_type; + } + if ( isset( self::$current_hook_source_stack[ current_filter() ] ) ) { + $sources = self::$current_hook_source_stack[ current_filter() ]; + array_pop( $sources ); // Remove self. + $source['sources'] = $sources; + } + return implode( '', array( + self::get_source_comment( $source, true ), + $value, + self::get_source_comment( $source, false ), + ) ); + } + + /** + * 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 (core, plugin, mu-plugin, or theme). + * @type string $name Source name. + * @type string $function Normalized function name. + * @type ReflectionMethod|ReflectionFunction $reflection + * } + */ + public static function get_source( $callback ) { + $reflection = null; + $class_name = null; // Because ReflectionMethod::getDeclaringClass() can return a parent class. + try { + if ( is_string( $callback ) && is_callable( $callback ) ) { + // The $callback is a function or static method. + $exploded_callback = explode( '::', $callback, 2 ); + if ( 2 === count( $exploded_callback ) ) { + $class_name = $exploded_callback[0]; + $reflection = new ReflectionMethod( $exploded_callback[0], $exploded_callback[1] ); + } 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. + if ( is_string( $callback[0] ) ) { + $class_name = $callback[0]; + } elseif ( is_object( $callback[0] ) ) { + $class_name = get_class( $callback[0] ); + } + $reflection = new ReflectionMethod( $callback[0], $callback[1] ); + } elseif ( is_object( $callback ) && ( 'Closure' === get_class( $callback ) ) ) { + $reflection = new ReflectionFunction( $callback ); + } + } catch ( Exception $e ) { + return null; + } + + if ( ! $reflection ) { + return null; + } + + $source = compact( 'reflection' ); + + $file = $reflection->getFileName(); + if ( $file ) { + $file = wp_normalize_path( $file ); + $slug_pattern = '([^/]+)'; + if ( preg_match( ':' . preg_quote( trailingslashit( wp_normalize_path( WP_PLUGIN_DIR ) ), ':' ) . $slug_pattern . ':s', $file, $matches ) ) { + $source['type'] = 'plugin'; + $source['name'] = $matches[1]; + } elseif ( preg_match( ':' . preg_quote( trailingslashit( wp_normalize_path( get_theme_root() ) ), ':' ) . $slug_pattern . ':s', $file, $matches ) ) { + $source['type'] = 'theme'; + $source['name'] = $matches[1]; + } elseif ( preg_match( ':' . preg_quote( trailingslashit( wp_normalize_path( WPMU_PLUGIN_DIR ) ), ':' ) . $slug_pattern . ':s', $file, $matches ) ) { + $source['type'] = 'mu-plugin'; + $source['name'] = $matches[1]; + } elseif ( preg_match( ':' . preg_quote( trailingslashit( wp_normalize_path( ABSPATH ) ), ':' ) . '(wp-admin|wp-includes)/:s', $file, $matches ) ) { + $source['type'] = 'core'; + $source['name'] = $matches[1]; + } + } + + if ( $class_name ) { + $source['function'] = $class_name . '::' . $reflection->getName(); + } else { + $source['function'] = $reflection->getName(); + } + + return $source; + } + + /** + * Check whether or not output buffering is currently possible. + * + * This is to guard against a fatal error: "ob_start(): Cannot use output buffering in output buffering display handlers". + * + * @return bool Whether output buffering is allowed. + */ + public static function can_output_buffer() { + + // Output buffering for validation can only be done while overall output buffering is being done for the response. + if ( ! AMP_Theme_Support::is_output_buffering() ) { + return false; + } + + // Abort when in shutdown since output has finished, when we're likely in the overall output buffering display handler. + if ( did_action( 'shutdown' ) ) { + return false; + } + + // Check if any functions in call stack are output buffering display handlers. + $called_functions = array(); + if ( defined( 'DEBUG_BACKTRACE_IGNORE_ARGS' ) ) { + $arg = DEBUG_BACKTRACE_IGNORE_ARGS; // phpcs:ignore PHPCompatibility.PHP.NewConstants.debug_backtrace_ignore_argsFound + } else { + $arg = false; + } + $backtrace = debug_backtrace( $arg ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace -- Only way to find out if we are in a buffering display handler. + foreach ( $backtrace as $call_stack ) { + $called_functions[] = '{closure}' === $call_stack['function'] ? 'Closure::__invoke' : $call_stack['function']; + } + return 0 === count( array_intersect( ob_list_handlers(), $called_functions ) ); + } + + /** + * 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 array $source + * } + * @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; + } + + // Wrap the markup output of (action) hooks in source comments. + AMP_Validation_Manager::$hook_source_stack[] = $callback['source']; + $has_buffer_started = false; + if ( AMP_Validation_Manager::can_output_buffer() ) { + $has_buffer_started = ob_start( array( __CLASS__, 'wrap_buffer_with_source_comments' ) ); + } + $result = call_user_func_array( $function, array_slice( $args, 0, intval( $accepted_args ) ) ); + if ( $has_buffer_started ) { + ob_end_flush(); + } + array_pop( AMP_Validation_Manager::$hook_source_stack ); + + // 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 ) { + AMP_Validation_Manager::$enqueued_style_sources[ $handle ][] = array_merge( $callback['source'], compact( 'handle' ) ); + } + } + + // 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 $queued_handle ) { + $handles = array( $queued_handle ); + + // Account for case where registered script is a placeholder for a set of scripts (e.g. jquery). + if ( isset( $wp_scripts->registered[ $queued_handle ] ) && false === $wp_scripts->registered[ $queued_handle ]->src ) { + $handles = array_merge( $handles, $wp_scripts->registered[ $queued_handle ]->deps ); + } + + foreach ( $handles as $handle ) { + AMP_Validation_Manager::$enqueued_script_sources[ $handle ][] = array_merge( $callback['source'], compact( 'handle' ) ); + } + } + } + + return $result; + }; + } + + /** + * Wrap output buffer with source comments. + * + * A key reason for why this is a method and not a closure is so that + * the can_output_buffer method will be able to identify it by name. + * + * @since 0.7 + * @todo Is duplicate of \AMP_Validation_Manager::decorate_filter_source()? + * + * @param string $output Output buffer. + * @return string Output buffer conditionally wrapped with source comments. + */ + public static function wrap_buffer_with_source_comments( $output ) { + if ( empty( self::$hook_source_stack ) ) { + return $output; + } + + $source = self::$hook_source_stack[ count( self::$hook_source_stack ) - 1 ]; + + // Wrap output that contains HTML tags (as opposed to actions that trigger in HTML attributes). + if ( ! empty( $output ) && preg_match( '/<.+?>/s', $output ) ) { + $output = implode( '', array( + self::get_source_comment( $source, true ), + $output, + self::get_source_comment( $source, false ), + ) ); + } + return $output; + } + + /** + * 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. + * + * @return boolean Whether to validate. + */ + public static function should_validate_response() { + return self::has_cap() && isset( $_GET[ self::VALIDATE_QUERY_VAR ] ); // WPCS: CSRF ok. + } + + /** + * Determine if there are any validation errors which have not been ignored. + * + * @return bool Whether AMP is blocked. + */ + public static function has_blocking_validation_errors() { + foreach ( self::$validation_results as $result ) { + if ( false === $result['sanitized'] ) { + return true; + } + } + return false; + } + + /** + * 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 $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( + 'remove_source_comments' => ! self::$debug, + 'append_validation_status_comment' => true, + ), + $args + ); + + if ( $args['remove_source_comments'] ) { + self::remove_source_comments( $dom ); + } + + if ( $args['append_validation_status_comment'] ) { + $errors = wp_list_pluck( self::$validation_results, 'error' ); + $encoded = wp_json_encode( $errors, 128 /* JSON_PRETTY_PRINT */ ); + $encoded = str_replace( '--', '\u002d\u002d', $encoded ); // Prevent "--" in strings from breaking out of HTML comments. + $comment = $dom->createComment( 'AMP_VALIDATION_ERRORS:' . $encoded . "\n" ); + $dom->documentElement->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 ) { + $args['validation_error_callback'] = __CLASS__ . '::add_validation_error'; + } + + // @todo Pass this into all sanitizers? + if ( isset( $sanitizers['AMP_Style_Sanitizer'] ) ) { + $sanitizers['AMP_Style_Sanitizer']['locate_sources'] = true; + } + + return $sanitizers; + } + + /** + * 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 ), // @todo Passing-along the credentials of the currently-authenticated user prevents this from working in cron. + '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 ) + ); + } + $response = wp_remote_retrieve_body( $r ); + if ( ! preg_match( '#.*?#s', $response, $matches ) ) { + return new WP_Error( 'response_comment_absent' ); + } + $validation_errors = json_decode( $matches[1], 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 = AMP_Validation_Error_Taxonomy::summarize_validation_errors( $validation_errors ); + $invalid_plugins = isset( $errors[ AMP_Validation_Error_Taxonomy::SOURCES_INVALID_OUTPUT ]['plugin'] ) ? array_unique( $errors[ AMP_Validation_Error_Taxonomy::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( + '

      %s %s %s

      ', + 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. + } + } + } + + /** + * Get validation debug UR:. + * + * @param string $url URL to to validate and debug. + * @return string Debug URL. + */ + public static function get_debug_url( $url ) { + return add_query_arg( + array( + self::VALIDATE_QUERY_VAR => '', + self::DEBUG_QUERY_VAR => '', + ), + $url + ) . '#development=1'; + } + + /** + * Enqueues the block validation script. + * + * @return void + */ + public static function enqueue_block_validation() { + $slug = 'amp-block-validation'; + + wp_enqueue_script( + $slug, + amp_get_asset_url( "js/{$slug}.js" ), + array( 'underscore' ), + AMP__VERSION, + true + ); + + $data = wp_json_encode( array( + 'i18n' => gutenberg_get_jed_locale_data( 'amp' ), // @todo POT file. + 'ampValidityRestField' => self::VALIDITY_REST_FIELD_NAME, + ) ); + wp_add_inline_script( $slug, sprintf( 'ampBlockValidation.boot( %s );', $data ) ); + } +} diff --git a/tests/test-class-amp-base-sanitizer.php b/tests/test-class-amp-base-sanitizer.php index a4ec4490e7c..af245308c39 100644 --- a/tests/test-class-amp-base-sanitizer.php +++ b/tests/test-class-amp-base-sanitizer.php @@ -17,7 +17,7 @@ class AMP_Base_Sanitizer_Test extends WP_UnitTestCase { */ public function setUp() { parent::setUp(); - AMP_Validation_Utils::reset_validation_results(); + AMP_Validation_Manager::reset_validation_results(); } /** @@ -25,8 +25,8 @@ public function setUp() { */ public function tearDown() { parent::tearDown(); - AMP_Validation_Utils::reset_validation_results(); - AMP_Validation_Utils::$locate_sources = false; + AMP_Validation_Manager::reset_validation_results(); + AMP_Validation_Manager::$locate_sources = false; } /** @@ -212,13 +212,13 @@ public function test_remove_child() { $this->assertEquals( $child, $parent->firstChild ); $sanitizer = new AMP_Iframe_Sanitizer( $dom_document, array( - 'validation_error_callback' => 'AMP_Validation_Utils::add_validation_error', + 'validation_error_callback' => 'AMP_Validation_Manager::add_validation_error', ) ); $sanitizer->remove_invalid_child( $child ); $this->assertEquals( null, $parent->firstChild ); - $this->assertCount( 1, AMP_Validation_Utils::$validation_results ); - $this->assertEquals( $child->nodeName, AMP_Validation_Utils::$validation_results[0]['error']['node_name'] ); + $this->assertCount( 1, AMP_Validation_Manager::$validation_results ); + $this->assertEquals( $child->nodeName, AMP_Validation_Manager::$validation_results[0]['error']['node_name'] ); $parent->appendChild( $child ); $this->assertEquals( $child, $parent->firstChild ); @@ -226,7 +226,7 @@ public function test_remove_child() { $this->assertEquals( null, $parent->firstChild ); $this->assertEquals( null, $child->parentNode ); - AMP_Validation_Utils::$validation_results = null; + AMP_Validation_Manager::$validation_results = null; } /** @@ -235,7 +235,9 @@ public function test_remove_child() { * @covers AMP_Base_Sanitizer::remove_invalid_child() */ public function test_remove_attribute() { - AMP_Validation_Utils::$locate_sources = true; + $this->markTestSkipped( 'Needs refactoring.' ); + + AMP_Validation_Manager::$locate_sources = true; add_filter( 'amp_validation_error_sanitized', '__return_true' ); $video_name = 'amp-video'; $attribute = 'onload'; @@ -244,14 +246,14 @@ public function test_remove_attribute() { $video->setAttribute( $attribute, 'someFunction()' ); $attr_node = $video->getAttributeNode( $attribute ); $args = array( - 'validation_error_callback' => 'AMP_Validation_Utils::add_validation_error', + 'validation_error_callback' => 'AMP_Validation_Manager::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( - 'code' => AMP_Validation_Utils::INVALID_ATTRIBUTE_CODE, + 'code' => AMP_Validation_Manager::INVALID_ATTRIBUTE_CODE, 'node_name' => $attr_node->nodeName, 'parent_name' => $video->nodeName, 'sources' => array(), @@ -259,7 +261,7 @@ public function test_remove_attribute() { 'onload' => 'someFunction()', ), ), - AMP_Validation_Utils::$validation_results[0]['error'] + AMP_Validation_Manager::$validation_results[0]['error'] ); } } diff --git a/tests/test-class-amp-options-manager.php b/tests/test-class-amp-options-manager.php index 564077281ac..38cac867af2 100644 --- a/tests/test-class-amp-options-manager.php +++ b/tests/test-class-amp-options-manager.php @@ -225,4 +225,38 @@ public function test_check_supported_post_type_update_errors() { $this->assertEquals( 'foo_deactivation_error', $error['code'] ); $wp_settings_errors = array(); } + + /** + * Test for persistent_object_caching_notice() + * + * @covers AMP_Options_Manager::persistent_object_caching_notice() + */ + public function test_persistent_object_caching_notice() { + set_current_screen( 'toplevel_page_amp-options' ); + $text = 'The AMP plugin performs at its best when persistent object cache is enabled.'; + + wp_using_ext_object_cache( null ); + ob_start(); + AMP_Options_Manager::persistent_object_caching_notice(); + $this->assertContains( $text, ob_get_clean() ); + + wp_using_ext_object_cache( true ); + ob_start(); + AMP_Options_Manager::persistent_object_caching_notice(); + $this->assertNotContains( $text, ob_get_clean() ); + + set_current_screen( 'edit.php' ); + + wp_using_ext_object_cache( null ); + ob_start(); + AMP_Options_Manager::persistent_object_caching_notice(); + $this->assertNotContains( $text, ob_get_clean() ); + + wp_using_ext_object_cache( true ); + ob_start(); + AMP_Options_Manager::persistent_object_caching_notice(); + $this->assertNotContains( $text, ob_get_clean() ); + + wp_using_ext_object_cache( false ); + } } diff --git a/tests/test-class-amp-theme-support.php b/tests/test-class-amp-theme-support.php index 91e62c3a91d..3dd76ac3de0 100644 --- a/tests/test-class-amp-theme-support.php +++ b/tests/test-class-amp-theme-support.php @@ -25,7 +25,10 @@ class Test_AMP_Theme_Support extends WP_UnitTestCase { */ public function setUp() { parent::setUp(); - AMP_Validation_Utils::reset_validation_results(); + AMP_Validation_Manager::reset_validation_results(); + AMP_Validation_Manager::$debug = false; + unset( $GLOBALS['current_screen'] ); + remove_theme_support( 'amp' ); } /** @@ -37,7 +40,7 @@ public function tearDown() { global $wp_scripts; $wp_scripts = null; parent::tearDown(); - AMP_Validation_Utils::reset_validation_results(); + AMP_Validation_Manager::reset_validation_results(); remove_theme_support( 'amp' ); $_REQUEST = array(); // phpcs:ignore WordPress.CSRF.NonceVerification.NoNonceVerification $_SERVER['QUERY_STRING'] = ''; @@ -1054,7 +1057,7 @@ public function test_prepare_response() { $this->assertContains( '', $sanitized_html ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript $removed_nodes = array(); - foreach ( AMP_Validation_Utils::$validation_results as $result ) { + foreach ( AMP_Validation_Manager::$validation_results as $result ) { if ( $result['sanitized'] && isset( $result['error']['node_name'] ) ) { $node_name = $result['error']['node_name']; if ( ! isset( $removed_nodes[ $node_name ] ) ) { @@ -1065,7 +1068,7 @@ public function test_prepare_response() { } $this->assertContains( '', $sanitized_html ); - $this->assertCount( 5, AMP_Validation_Utils::$validation_results ); + $this->assertCount( 5, AMP_Validation_Manager::$validation_results ); $this->assertEquals( array( 'onclick' => 1, diff --git a/tests/test-class-amp-validation-utils.php b/tests/test-class-amp-validation-utils.php index 5a78428ab16..a40ca30f6b6 100644 --- a/tests/test-class-amp-validation-utils.php +++ b/tests/test-class-amp-validation-utils.php @@ -1,12 +1,12 @@ node = $dom_document->createElement( self::TAG_NAME ); - AMP_Validation_Utils::reset_validation_results(); + AMP_Validation_Manager::reset_validation_results(); $this->original_wp_registered_widgets = $GLOBALS['wp_registered_widgets']; } @@ -96,26 +96,28 @@ public function tearDown() { $GLOBALS['wp_registered_widgets'] = $this->original_wp_registered_widgets; // WPCS: override ok. remove_theme_support( 'amp' ); unset( $GLOBALS['current_screen'] ); - AMP_Validation_Utils::$locate_sources = false; + AMP_Validation_Manager::$locate_sources = false; parent::tearDown(); } /** * Test init. * - * @covers AMP_Validation_Utils::init() + * @covers AMP_Validation_Manager::init() */ public function test_init() { + $this->markTestSkipped( 'Needs refactoring' ); + add_theme_support( 'amp' ); - AMP_Validation_Utils::init(); + AMP_Validation_Manager::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_filter( 'manage_' . AMP_Validation_Manager::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_filter( 'bulk_actions-edit-' . AMP_Validation_Manager::POST_TYPE_SLUG, self::TESTED_CLASS . '::add_bulk_action' ) ); + $this->assertEquals( 10, has_filter( 'handle_bulk_actions-edit-' . AMP_Validation_Manager::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_notices', self::TESTED_CLASS . '::persistent_object_caching_notice' ) ); $this->assertEquals( 10, has_action( 'admin_menu', self::TESTED_CLASS . '::remove_publish_meta_box' ) ); @@ -126,10 +128,10 @@ public function test_init() { /** * Test add_validation_hooks. * - * @covers AMP_Validation_Utils::add_validation_hooks() + * @covers AMP_Validation_Manager::add_validation_hooks() */ public function test_add_validation_hooks() { - AMP_Validation_Utils::add_validation_hooks(); + AMP_Validation_Manager::add_validation_hooks(); $this->assertEquals( PHP_INT_MAX, has_filter( 'the_content', array( self::TESTED_CLASS, 'decorate_filter_source' ) ) ); $this->assertEquals( PHP_INT_MAX, has_filter( 'the_excerpt', array( self::TESTED_CLASS, 'decorate_filter_source' ) ) ); $this->assertEquals( -1, has_action( 'do_shortcode_tag', array( self::TESTED_CLASS, 'decorate_shortcode_source' ) ) ); @@ -138,7 +140,7 @@ public function test_add_validation_hooks() { /** * Test add_validation_hooks with Gutenberg active. * - * @covers AMP_Validation_Utils::add_validation_hooks() + * @covers AMP_Validation_Manager::add_validation_hooks() */ public function test_add_validation_hooks_gutenberg() { if ( ! function_exists( 'do_blocks' ) ) { @@ -150,7 +152,7 @@ public function test_add_validation_hooks_gutenberg() { $priority = has_filter( 'the_content', 'do_blocks' ); $this->assertNotFalse( $priority ); - AMP_Validation_Utils::add_validation_hooks(); + AMP_Validation_Manager::add_validation_hooks(); $this->assertEquals( $priority - 1, has_filter( 'the_content', array( self::TESTED_CLASS, 'add_block_source_comments' ) ) ); } @@ -198,8 +200,9 @@ public function get_block_data() { * @param string $content Content. * @param string $expected Expected content. * @param array $query Query. + * * @dataProvider get_block_data - * @covers AMP_Validation_Utils::add_block_source_comments() + * @covers AMP_Validation_Manager::add_block_source_comments() */ public function test_add_block_source_comments( $content, $expected, $query ) { if ( ! function_exists( 'do_blocks' ) ) { @@ -213,7 +216,7 @@ public function test_add_block_source_comments( $content, $expected, $query ) { $post = $this->factory()->post->create_and_get(); // WPCS: Override ok. $this->assertInstanceOf( 'WP_Post', get_post() ); - $rendered_block = do_blocks( AMP_Validation_Utils::add_block_source_comments( $content ) ); + $rendered_block = do_blocks( AMP_Validation_Manager::add_block_source_comments( $content ) ); $expected = str_replace( array( @@ -238,22 +241,24 @@ public function test_add_block_source_comments( $content, $expected, $query ) { $this->assertEquals( $query['blocks'], - wp_list_pluck( AMP_Validation_Utils::locate_sources( $el ), 'block_name' ) + wp_list_pluck( AMP_Validation_Manager::locate_sources( $el ), 'block_name' ) ); } /** * Test add_validation_error. * - * @covers AMP_Validation_Utils::add_validation_error() + * @covers AMP_Validation_Manager::add_validation_error() */ public function test_track_removed() { - AMP_Validation_Utils::$locate_sources = true; - $this->assertEmpty( AMP_Validation_Utils::$validation_results ); - AMP_Validation_Utils::add_validation_error( + $this->markTestSkipped( 'Needs refactoring' ); + + AMP_Validation_Manager::$locate_sources = true; + $this->assertEmpty( AMP_Validation_Manager::$validation_results ); + AMP_Validation_Manager::add_validation_error( array( 'node_name' => $this->node->nodeName, - 'code' => AMP_Validation_Utils::INVALID_ELEMENT_CODE, + 'code' => AMP_Validation_Manager::INVALID_ELEMENT_CODE, 'node_attributes' => array(), ), array( @@ -261,108 +266,110 @@ public function test_track_removed() { ) ); - $this->assertCount( 1, AMP_Validation_Utils::$validation_results ); + $this->assertCount( 1, AMP_Validation_Manager::$validation_results ); $this->assertEquals( array( 'node_name' => 'img', 'sources' => array(), - 'code' => AMP_Validation_Utils::INVALID_ELEMENT_CODE, + 'code' => AMP_Validation_Manager::INVALID_ELEMENT_CODE, 'node_attributes' => array(), ), - AMP_Validation_Utils::$validation_results[0]['error'] + AMP_Validation_Manager::$validation_results[0]['error'] ); - AMP_Validation_Utils::reset_validation_results(); + AMP_Validation_Manager::reset_validation_results(); } /** * Test was_node_removed. * - * @covers AMP_Validation_Utils::add_validation_error() + * @covers AMP_Validation_Manager::add_validation_error() */ public function test_was_node_removed() { - $this->assertEmpty( AMP_Validation_Utils::$validation_results ); - AMP_Validation_Utils::add_validation_error( + $this->assertEmpty( AMP_Validation_Manager::$validation_results ); + AMP_Validation_Manager::add_validation_error( array( 'node' => $this->node, ) ); - $this->assertNotEmpty( AMP_Validation_Utils::$validation_results ); + $this->assertNotEmpty( AMP_Validation_Manager::$validation_results ); } /** * Test process_markup. * - * @covers AMP_Validation_Utils::process_markup() + * @covers AMP_Validation_Manager::process_markup() */ public function test_process_markup() { add_filter( 'amp_validation_error_sanitized', '__return_true' ); $this->set_capability(); - AMP_Validation_Utils::process_markup( $this->valid_amp_img ); - $this->assertEquals( array(), AMP_Validation_Utils::$validation_results ); + AMP_Validation_Manager::process_markup( $this->valid_amp_img ); + $this->assertEquals( array(), AMP_Validation_Manager::$validation_results ); - AMP_Validation_Utils::reset_validation_results(); + AMP_Validation_Manager::reset_validation_results(); $video = '
    • @@ -873,16 +965,16 @@ public static function print_validation_errors_meta_box( $post ) {
    • $value ) : ?>
    • -
      > +
      >
      @@ -973,7 +1065,7 @@ public static function filter_dashboard_glance_items( $items ) { $query = new WP_Query( array( 'post_type' => self::POST_TYPE_SLUG, - AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_NEW_STATUS, + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); @@ -985,7 +1077,7 @@ public static function filter_dashboard_glance_items( $items ) { add_query_arg( array( 'post_type' => self::POST_TYPE_SLUG, - AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_NEW_STATUS, + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS, ), 'edit.php' ) diff --git a/includes/validation/class-amp-validation-error-taxonomy.php b/includes/validation/class-amp-validation-error-taxonomy.php index a21182e3105..506a7e89d8c 100644 --- a/includes/validation/class-amp-validation-error-taxonomy.php +++ b/includes/validation/class-amp-validation-error-taxonomy.php @@ -155,358 +155,11 @@ public static function register() { ), ) ); - // Include searching taxonomy term descriptions and sources term meta. - add_filter( 'terms_clauses', function( $clauses, $taxonomies, $args ) { - global $wpdb; - if ( ! empty( $args['search'] ) && in_array( self::TAXONOMY_SLUG, $taxonomies, true ) ) { - $clauses['where'] = preg_replace( - '#(?<=\()(?=\(t\.name LIKE \')#', - $wpdb->prepare( '(tt.description LIKE %s) OR ', '%' . $wpdb->esc_like( $args['search'] ) . '%' ), - $clauses['where'] - ); - } - return $clauses; - }, 10, 3 ); - - // Hide empty term addition form. - add_action( 'admin_enqueue_scripts', function() { - if ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy ) { - wp_add_inline_style( 'common', ' - #col-left { display: none; } - #col-right { float:none; width: auto; } - - /* Improve column widths */ - td.column-details pre, td.column-sources pre { overflow:auto; } - th.column-created_date_gmt { width:15%; } - th.column-status { width:10%; } - ' ); - } - } ); - - // Make sure parent menu item is expanded when visiting the taxonomy term page. - add_filter( 'parent_file', function( $parent_file ) { - if ( get_current_screen()->taxonomy === self::TAXONOMY_SLUG ) { - $parent_file = AMP_Options_Manager::OPTION_NAME; - } - return $parent_file; - }, 10, 2 ); - - // Replace the primary column to be error instead of the removed name column.. - add_filter( 'list_table_primary_column', function( $primary_column ) { - if ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy ) { - $primary_column = 'error'; - } - return $primary_column; - } ); - - add_filter( 'posts_where', array( __CLASS__, 'filter_posts_where_for_validation_error_status' ), 10, 2 ); - - add_filter( 'views_edit-' . self::TAXONOMY_SLUG, array( __CLASS__, 'filter_views_edit' ) ); - - // Override the columns displayed for the validation error terms. - add_filter( 'manage_edit-' . self::TAXONOMY_SLUG . '_columns', function( $old_columns ) { - return array( - 'cb' => $old_columns['cb'], - 'error' => __( 'Error', 'amp' ), - 'created_date_gmt' => __( 'Created Date', 'amp' ), - 'status' => __( 'Status', 'amp' ), - 'details' => __( 'Details', 'amp' ), - 'posts' => __( 'URLs', 'amp' ), - ); - } ); - - // Let the created date column sort by term ID. - add_filter( 'manage_edit-' . self::TAXONOMY_SLUG . '_sortable_columns', function( $sortable_columns ) { - $sortable_columns['created_date_gmt'] = 'term_id'; - return $sortable_columns; - } ); - - add_filter( 'manage_' . self::TAXONOMY_SLUG . '_custom_column', array( __CLASS__, 'filter_manage_custom_columns' ), 10, 3 ); - add_action( 'load-edit-tags.php', array( __CLASS__, 'prevent_bulk_deleting_non_empty_terms' ) ); - add_action( 'admin_notices', array( __CLASS__, 'show_bulk_delete_blocked_error_notice' ) ); - - // Prevent user from being able to delete validation errors when they still have associated invalid URLs. - add_filter( 'user_has_cap', function( $allcaps, $caps, $args ) { - if ( isset( $args[0] ) && 'delete_term' === $args[0] && 0 !== get_term( $args[2] )->count ) { - /* - * However, only apply this if not on the edit terms screen for validation errors, since - * WP_Terms_List_Table::column_cb() unfortunately has a hard-coded delete_term capability check, so - * without that check passing then the checkbox is not shown. - */ - if ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy && empty( $_REQUEST['action'] ) ) { - return $allcaps; - } + add_filter( 'user_has_cap', array( __CLASS__, 'filter_user_has_cap_for_deletion' ), 10, 3 ); - $allcaps = array_merge( - $allcaps, - array_fill_keys( $caps, false ) - ); - } - return $allcaps; - }, 10, 3 ); - - // Add row actions. - add_filter( 'tag_row_actions', function( $actions, WP_Term $tag ) { - if ( self::TAXONOMY_SLUG === $tag->taxonomy ) { - $term_id = $tag->term_id; - - /* - * Hide deletion link when there are remaining invalid URLs associated with them. - * Note that this would normally be handled via the user_has_cap filter above, - * but this has to be here due to a problem with WP_Terms_List_Table::column_cb() - * which requires a workaround. - */ - if ( 0 !== $tag->count ) { - unset( $actions['delete'] ); - } - - if ( self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS !== $tag->term_group ) { - $actions[ self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = sprintf( - '%s', - wp_nonce_url( - add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ), compact( 'term_id' ) ) ), - self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION - ), - esc_attr__( 'Acknowledging an error marks it as read. AMP validation errors prevent a URL from being served as AMP.', 'amp' ), - esc_html__( 'Acknowledge', 'amp' ) - ); - } - if ( self::VALIDATION_ERROR_IGNORED_STATUS !== $tag->term_group ) { - $actions[ self::VALIDATION_ERROR_IGNORE_ACTION ] = sprintf( - '%s', - wp_nonce_url( - add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_IGNORE_ACTION ), compact( 'term_id' ) ) ), - self::VALIDATION_ERROR_IGNORE_ACTION - ), - esc_attr__( 'Ignoring an error prevents it from blocking a URL from being served as AMP.', 'amp' ), - esc_html__( 'Ignore', 'amp' ) - ); - } - } - return $actions; - }, 10, 2 ); - - // Filter amp_validation_error term query by term group when requested. - add_filter( 'get_terms_defaults', function( $args, $taxonomies ) { - if ( array( AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ) === $taxonomies ) { - $args['orderby'] = 'term_id'; - $args['order'] = 'DESC'; - } - return $args; - }, 10, 2 ); - - // Filter amp_validation_error term query by term group when requested. - add_action( 'load-edit-tags.php', function() { - if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || ! isset( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ) ) { // WPCS: CSRF ok. - return; - } - self::$should_filter_terms_clauses_for_error_validation_status = true; - $group = intval( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ); // WPCS: CSRF ok. - if ( ! in_array( $group, array( self::VALIDATION_ERROR_NEW_STATUS, self::VALIDATION_ERROR_IGNORED_STATUS, self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ), true ) ) { - return; - } - add_filter( 'terms_clauses', function( $clauses, $taxonomies ) use ( $group ) { - global $wpdb; - if ( self::TAXONOMY_SLUG === $taxonomies[0] && self::$should_filter_terms_clauses_for_error_validation_status ) { - $clauses['where'] .= $wpdb->prepare( ' AND t.term_group = %d', $group ); - } - return $clauses; - }, 10, 2 ); - } ); - - // Handle inline edit links. - add_action( 'load-edit-tags.php', function() { - if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || ! isset( $_GET['action'] ) || ! isset( $_GET['_wpnonce'] ) || ! isset( $_GET['term_id'] ) ) { // WPCS: CSRF ok. - return; - } - $action = sanitize_key( $_GET['action'] ); // WPCS: CSRF ok. - check_admin_referer( $action ); - $tax = get_taxonomy( self::TAXONOMY_SLUG ); - if ( ! current_user_can( $tax->cap->manage_terms ) ) { // Yes it is an object. - return; - } - - $referer = wp_get_referer(); - $term_id = intval( $_GET['term_id'] ); // WPCS: CSRF ok. - $redirect = self::handle_validation_error_update( $referer, $action, array( $term_id ) ); - - if ( $redirect !== $referer ) { - wp_safe_redirect( $redirect ); - exit; - } - } ); - - // Add bulk actions. - add_filter( 'bulk_actions-edit-' . self::TAXONOMY_SLUG, function( $bulk_actions ) { - $bulk_actions[ self::VALIDATION_ERROR_IGNORE_ACTION ] = __( 'Ignore', 'amp' ); - $bulk_actions[ self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = __( 'Acknowledge', 'amp' ); - return $bulk_actions; - } ); - - // Handle bulk actions. - add_filter( 'handle_bulk_actions-edit-' . self::TAXONOMY_SLUG, array( __CLASS__, 'handle_validation_error_update' ), 10, 3 ); - - // Prevent query vars from persisting after redirect. - add_filter( 'removable_query_args', function( $query_vars ) { - $query_vars[] = 'amp_actioned'; - $query_vars[] = 'amp_actioned_count'; - $query_vars[] = 'amp_validation_errors_not_deleted'; - return $query_vars; - } ); - - // Show notices for changes to amp_validation_error terms. - add_action( 'admin_notices', function() { - if ( ! ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy || AMP_Invalid_URL_Post_Type::POST_TYPE_SLUG === get_current_screen()->post_type ) || empty( $_GET['amp_actioned'] ) || empty( $_GET['amp_actioned_count'] ) ) { // WPCS: CSRF ok. - return; - } - $actioned = sanitize_key( $_GET['amp_actioned'] ); // WPCS: CSRF ok. - $count = intval( $_GET['amp_actioned_count'] ); // WPCS: CSRF ok. - $message = null; - if ( self::VALIDATION_ERROR_IGNORE_ACTION === $actioned ) { - $message = sprintf( - /* translators: %s is number of errors ignored */ - _n( - 'Ignored %s error. It will no longer block related URLs from being served as AMP.', - 'Ignored %s errors. They will no longer block related URLs from being served as AMP.', - number_format_i18n( $count ), - 'amp' - ), - $count - ); - } elseif ( self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION === $actioned ) { - $message = sprintf( - /* translators: %s is number of errors acknowledged */ - _n( - 'Acknowledged %s error. It will continue to block related URLs from being served as AMP.', - 'Acknowledged %s errors. They will continue to block related URLs from being served as AMP.', - number_format_i18n( $count ), - 'amp' - ), - $count - ); - } - - if ( $message ) { - printf( '

      %s

      ', esc_html( $message ) ); - } - } ); - - // Add recognition of amp_validation_error_status query var (which will only apply in admin since post type is not publicly_queryable). - add_filter( 'query_vars', function( $query_vars ) { - $query_vars[] = self::VALIDATION_ERROR_STATUS_QUERY_VAR; - return $query_vars; - } ); - - // Add views for filtering validation errors by status. - add_filter( 'views_edit-' . AMP_Invalid_URL_Post_Type::POST_TYPE_SLUG, function( $views ) { - unset( $views['publish'] ); - - $args = array( - 'post_type' => AMP_Invalid_URL_Post_Type::POST_TYPE_SLUG, - 'update_post_meta_cache' => false, - 'update_post_term_cache' => false, - ); - - $with_new_query = new WP_Query( array_merge( - $args, - array( self::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_NEW_STATUS ) - ) ); - $with_acknowledged_query = new WP_Query( array_merge( - $args, - array( self::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ) - ) ); - $with_ignored_query = new WP_Query( array_merge( - $args, - array( self::VALIDATION_ERROR_STATUS_QUERY_VAR => self::VALIDATION_ERROR_IGNORED_STATUS ) - ) ); - - $current_url = remove_query_arg( - array_merge( - wp_removable_query_args(), - array( 's' ) // For some reason behavior of posts list table is to not persist the search query. - ), - wp_unslash( $_SERVER['REQUEST_URI'] ) - ); - - $current_status = null; - if ( isset( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ) ) { // WPCS: CSRF ok. - $value = intval( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ); // WPCS: CSRF ok. - if ( in_array( $value, array( self::VALIDATION_ERROR_NEW_STATUS, self::VALIDATION_ERROR_IGNORED_STATUS, self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ), true ) ) { - $current_status = $value; - } - } - - $views['new'] = sprintf( - '%s', - esc_url( - add_query_arg( - self::VALIDATION_ERROR_STATUS_QUERY_VAR, - self::VALIDATION_ERROR_NEW_STATUS, - $current_url - ) - ), - self::VALIDATION_ERROR_NEW_STATUS === $current_status ? 'current' : '', - sprintf( - /* translators: %s is the post count */ - _nx( - 'With New Errors (%s)', - 'With New Errors (%s)', - $with_new_query->found_posts, - 'posts', - 'amp' - ), - number_format_i18n( $with_new_query->found_posts ) - ) - ); - - $views['acknowledged'] = sprintf( - '%s', - esc_url( - add_query_arg( - self::VALIDATION_ERROR_STATUS_QUERY_VAR, - self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS, - $current_url - ) - ), - self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS === $current_status ? 'current' : '', - sprintf( - /* translators: %s is the post count */ - _nx( - 'With Acknowledged Errors (%s)', - 'With Acknowledged Errors (%s)', - $with_acknowledged_query->found_posts, - 'posts', - 'amp' - ), - number_format_i18n( $with_acknowledged_query->found_posts ) - ) - ); - - $views['ignored'] = sprintf( - '%s', - esc_url( - add_query_arg( - self::VALIDATION_ERROR_STATUS_QUERY_VAR, - self::VALIDATION_ERROR_IGNORED_STATUS, - $current_url - ) - ), - self::VALIDATION_ERROR_IGNORED_STATUS === $current_status ? 'current' : '', - sprintf( - /* translators: %s is the post count */ - _nx( - 'With Ignored Errors (%s)', - 'With Ignored Errors (%s)', - $with_ignored_query->found_posts, - 'posts', - 'amp' - ), - number_format_i18n( $with_ignored_query->found_posts ) - ) - ); - - return $views; - } ); + if ( is_admin() ) { + self::add_admin_hooks(); + } } /** @@ -636,6 +289,280 @@ public static function summarize_validation_errors( $validation_errors ) { return $results; } + /** + * Prevent user from being able to delete validation errors when they still have associated invalid URLs. + * + * @param array $allcaps All caps. + * @param array $caps Requested caps. + * @param array $args Cap args. + * @return array All caps. + */ + public static function filter_user_has_cap_for_deletion( $allcaps, $caps, $args ) { + if ( isset( $args[0] ) && 'delete_term' === $args[0] && 0 !== get_term( $args[2] )->count ) { + /* + * However, only apply this if not on the edit terms screen for validation errors, since + * WP_Terms_List_Table::column_cb() unfortunately has a hard-coded delete_term capability check, so + * without that check passing then the checkbox is not shown. + */ + if ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy && empty( $_REQUEST['action'] ) ) { // WPCS: CSRF OK. + return $allcaps; + } + + $allcaps = array_merge( + $allcaps, + array_fill_keys( $caps, false ) + ); + } + return $allcaps; + } + + /** + * Add admin hooks. + */ + public static function add_admin_hooks() { + add_action( 'load-edit-tags.php', array( __CLASS__, 'add_group_terms_clauses_filter' ) ); + add_filter( 'terms_clauses', array( __CLASS__, 'filter_terms_clauses_for_description_search' ), 10, 3 ); + add_action( 'admin_notices', array( __CLASS__, 'add_admin_notices' ) ); + add_filter( 'tag_row_actions', array( __CLASS__, 'filter_tag_row_actions' ), 10, 2 ); + add_action( 'admin_menu', array( __CLASS__, 'add_admin_menu_validation_error_item' ) ); + add_filter( 'manage_' . self::TAXONOMY_SLUG . '_custom_column', array( __CLASS__, 'filter_manage_custom_columns' ), 10, 3 ); + add_action( 'load-edit-tags.php', array( __CLASS__, 'prevent_bulk_deleting_non_empty_terms' ) ); + add_action( 'admin_notices', array( __CLASS__, 'show_bulk_delete_blocked_error_notice' ) ); + add_filter( 'views_edit-' . self::TAXONOMY_SLUG, array( __CLASS__, 'filter_views_edit' ) ); + add_filter( 'posts_where', array( __CLASS__, 'filter_posts_where_for_validation_error_status' ), 10, 2 ); + add_filter( 'handle_bulk_actions-edit-' . self::TAXONOMY_SLUG, array( __CLASS__, 'handle_validation_error_update' ), 10, 3 ); + add_action( 'load-edit-tags.php', array( __CLASS__, 'handle_inline_edit_request' ) ); + + // Prevent query vars from persisting after redirect. + add_filter( 'removable_query_args', function( $query_vars ) { + $query_vars[] = 'amp_actioned'; + $query_vars[] = 'amp_actioned_count'; + $query_vars[] = 'amp_validation_errors_not_deleted'; + return $query_vars; + } ); + + // Add recognition of amp_validation_error_status query var (which will only apply in admin since post type is not publicly_queryable). + add_filter( 'query_vars', function( $query_vars ) { + $query_vars[] = self::VALIDATION_ERROR_STATUS_QUERY_VAR; + return $query_vars; + } ); + + // Filter amp_validation_error term query by term group when requested. + add_filter( 'get_terms_defaults', function( $args, $taxonomies ) { + if ( array( AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ) === $taxonomies ) { + $args['orderby'] = 'term_id'; + $args['order'] = 'DESC'; + } + return $args; + }, 10, 2 ); + + // Add bulk actions. + add_filter( 'bulk_actions-edit-' . self::TAXONOMY_SLUG, function( $bulk_actions ) { + $bulk_actions[ self::VALIDATION_ERROR_IGNORE_ACTION ] = __( 'Ignore', 'amp' ); + $bulk_actions[ self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = __( 'Acknowledge', 'amp' ); + return $bulk_actions; + } ); + + // Override the columns displayed for the validation error terms. + add_filter( 'manage_edit-' . self::TAXONOMY_SLUG . '_columns', function( $old_columns ) { + return array( + 'cb' => $old_columns['cb'], + 'error' => __( 'Error', 'amp' ), + 'created_date_gmt' => __( 'Created Date', 'amp' ), + 'status' => __( 'Status', 'amp' ), + 'details' => __( 'Details', 'amp' ), + 'posts' => __( 'URLs', 'amp' ), + ); + } ); + + // Let the created date column sort by term ID. + add_filter( 'manage_edit-' . self::TAXONOMY_SLUG . '_sortable_columns', function( $sortable_columns ) { + $sortable_columns['created_date_gmt'] = 'term_id'; + return $sortable_columns; + } ); + + // Hide empty term addition form. + add_action( 'admin_enqueue_scripts', function() { + if ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy ) { + wp_add_inline_style( 'common', ' + #col-left { display: none; } + #col-right { float:none; width: auto; } + + /* Improve column widths */ + td.column-details pre, td.column-sources pre { overflow:auto; } + th.column-created_date_gmt { width:15%; } + th.column-status { width:10%; } + ' ); + } + } ); + + // Make sure parent menu item is expanded when visiting the taxonomy term page. + add_filter( 'parent_file', function( $parent_file ) { + if ( get_current_screen()->taxonomy === self::TAXONOMY_SLUG ) { + $parent_file = AMP_Options_Manager::OPTION_NAME; + } + return $parent_file; + }, 10, 2 ); + + // Replace the primary column to be error instead of the removed name column.. + add_filter( 'list_table_primary_column', function( $primary_column ) { + if ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy ) { + $primary_column = 'error'; + } + return $primary_column; + } ); + } + + /** + * Filter amp_validation_error term query by term group when requested. + */ + public static function add_group_terms_clauses_filter() { + if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || ! isset( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ) ) { // WPCS: CSRF ok. + return; + } + self::$should_filter_terms_clauses_for_error_validation_status = true; + $group = intval( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ); // WPCS: CSRF ok. + if ( ! in_array( $group, array( self::VALIDATION_ERROR_NEW_STATUS, self::VALIDATION_ERROR_IGNORED_STATUS, self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ), true ) ) { + return; + } + add_filter( 'terms_clauses', function( $clauses, $taxonomies ) use ( $group ) { + global $wpdb; + if ( self::TAXONOMY_SLUG === $taxonomies[0] && self::$should_filter_terms_clauses_for_error_validation_status ) { + $clauses['where'] .= $wpdb->prepare( ' AND t.term_group = %d', $group ); + } + return $clauses; + }, 10, 2 ); + } + + /** + * Include searching taxonomy term descriptions and sources term meta. + * + * @param array $clauses Clauses. + * @param array $taxonomies Taxonomies. + * @param array $args Args. + * @return array Clauses. + */ + public static function filter_terms_clauses_for_description_search( $clauses, $taxonomies, $args ) { + global $wpdb; + if ( ! empty( $args['search'] ) && in_array( self::TAXONOMY_SLUG, $taxonomies, true ) ) { + $clauses['where'] = preg_replace( + '#(?<=\()(?=\(t\.name LIKE \')#', + $wpdb->prepare( '(tt.description LIKE %s) OR ', '%' . $wpdb->esc_like( $args['search'] ) . '%' ), + $clauses['where'] + ); + } + return $clauses; + } + + /** + * Show notices for changes to amp_validation_error terms. + */ + public static function add_admin_notices() { + if ( ! ( self::TAXONOMY_SLUG === get_current_screen()->taxonomy || AMP_Invalid_URL_Post_Type::POST_TYPE_SLUG === get_current_screen()->post_type ) || empty( $_GET['amp_actioned'] ) || empty( $_GET['amp_actioned_count'] ) ) { // WPCS: CSRF ok. + return; + } + $actioned = sanitize_key( $_GET['amp_actioned'] ); // WPCS: CSRF ok. + $count = intval( $_GET['amp_actioned_count'] ); // WPCS: CSRF ok. + $message = null; + if ( self::VALIDATION_ERROR_IGNORE_ACTION === $actioned ) { + $message = sprintf( + /* translators: %s is number of errors ignored */ + _n( + 'Ignored %s error. It will no longer block related URLs from being served as AMP.', + 'Ignored %s errors. They will no longer block related URLs from being served as AMP.', + number_format_i18n( $count ), + 'amp' + ), + $count + ); + } elseif ( self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION === $actioned ) { + $message = sprintf( + /* translators: %s is number of errors acknowledged */ + _n( + 'Acknowledged %s error. It will continue to block related URLs from being served as AMP.', + 'Acknowledged %s errors. They will continue to block related URLs from being served as AMP.', + number_format_i18n( $count ), + 'amp' + ), + $count + ); + } + + if ( $message ) { + printf( '

      %s

      ', esc_html( $message ) ); + } + } + + /** + * Add row actions. + * + * @param array $actions Actions. + * @param WP_Term $tag Tag. + * @return array Actions. + */ + public static function filter_tag_row_actions( $actions, WP_Term $tag ) { + if ( self::TAXONOMY_SLUG === $tag->taxonomy ) { + $term_id = $tag->term_id; + + /* + * Hide deletion link when there are remaining invalid URLs associated with them. + * Note that this would normally be handled via the user_has_cap filter above, + * but this has to be here due to a problem with WP_Terms_List_Table::column_cb() + * which requires a workaround. + */ + if ( 0 !== $tag->count ) { + unset( $actions['delete'] ); + } + + if ( self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS !== $tag->term_group ) { + $actions[ self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = sprintf( + '%s', + wp_nonce_url( + add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ), compact( 'term_id' ) ) ), + self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION + ), + esc_attr__( 'Acknowledging an error marks it as read. AMP validation errors prevent a URL from being served as AMP.', 'amp' ), + esc_html__( 'Acknowledge', 'amp' ) + ); + } + if ( self::VALIDATION_ERROR_IGNORED_STATUS !== $tag->term_group ) { + $actions[ self::VALIDATION_ERROR_IGNORE_ACTION ] = sprintf( + '%s', + wp_nonce_url( + add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_IGNORE_ACTION ), compact( 'term_id' ) ) ), + self::VALIDATION_ERROR_IGNORE_ACTION + ), + esc_attr__( 'Ignoring an error prevents it from blocking a URL from being served as AMP.', 'amp' ), + esc_html__( 'Ignore', 'amp' ) + ); + } + } + return $actions; + } + + /** + * Show AMP validation errors under AMP admin menu. + */ + public static function add_admin_menu_validation_error_item() { + $menu_item_label = esc_html__( 'Validation Errors', 'amp' ); + $new_error_count = self::get_validation_error_count( array( + 'group' => self::VALIDATION_ERROR_NEW_STATUS, + 'ignore_empty' => true, + ) ); + if ( $new_error_count ) { + $menu_item_label .= ' ' . esc_html( number_format_i18n( $new_error_count ) ) . ''; + } + + add_submenu_page( + AMP_Options_Manager::OPTION_NAME, + esc_html__( 'Validation Errors', 'amp' ), + $menu_item_label, + get_taxonomy( self::TAXONOMY_SLUG )->cap->manage_terms, // Yes, cap is an object not an array. + // The following esc_attr() is sadly needed due to . + esc_attr( 'edit-tags.php?taxonomy=' . self::TAXONOMY_SLUG . '&post_type=' . AMP_Invalid_URL_Post_Type::POST_TYPE_SLUG ) + ); + } + /** * Add views for filtering validation errors by status. * @@ -904,6 +831,30 @@ public static function show_bulk_delete_blocked_error_notice() { ); } + /** + * Handle inline edit links. + */ + public static function handle_inline_edit_request() { + if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy || ! isset( $_GET['action'] ) || ! isset( $_GET['_wpnonce'] ) || ! isset( $_GET['term_id'] ) ) { // WPCS: CSRF ok. + return; + } + $action = sanitize_key( $_GET['action'] ); // WPCS: CSRF ok. + check_admin_referer( $action ); + $tax = get_taxonomy( self::TAXONOMY_SLUG ); + if ( ! current_user_can( $tax->cap->manage_terms ) ) { // Yes it is an object. + return; + } + + $referer = wp_get_referer(); + $term_id = intval( $_GET['term_id'] ); // WPCS: CSRF ok. + $redirect = self::handle_validation_error_update( $referer, $action, array( $term_id ) ); + + if ( $redirect !== $referer ) { + wp_safe_redirect( $redirect ); + exit; + } + } + /** * Handle bulk and inline edits to amp_validation_error terms. * @@ -943,5 +894,4 @@ public static function handle_validation_error_update( $redirect_to, $action, $t return $redirect_to; } - } From 13c14a9132433489ed8747e05797facbdffc2df9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 24 May 2018 22:20:27 -0700 Subject: [PATCH 31/55] Rename "Acknowledge" with "Reject", and "Ignore" with "Accept" --- .../class-amp-invalid-url-post-type.php | 110 ++++++++-------- .../class-amp-validation-error-taxonomy.php | 118 +++++++++--------- .../class-amp-validation-manager.php | 2 +- 3 files changed, 115 insertions(+), 115 deletions(-) diff --git a/includes/validation/class-amp-invalid-url-post-type.php b/includes/validation/class-amp-invalid-url-post-type.php index 5f3e6da6981..ac9d31bfb70 100644 --- a/includes/validation/class-amp-invalid-url-post-type.php +++ b/includes/validation/class-amp-invalid-url-post-type.php @@ -166,14 +166,14 @@ public static function add_admin_menu_new_invalid_url_count() { * @param array $args { * Args. * - * @type bool $ignore_ignored Exclude validation errors that are ignored. Default false. + * @type bool $ignore_accepted Exclude validation errors that are accepted. Default false. * } * @return array List of errors. */ public static function get_invalid_url_validation_errors( $post, $args = array() ) { $args = array_merge( array( - 'ignore_ignored' => false, + 'ignore_accepted' => false, ), $args ); @@ -192,7 +192,7 @@ public static function get_invalid_url_validation_errors( $post, $args = array() if ( ! $term ) { continue; } - if ( $args['ignore_ignored'] && AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS === $term->term_group ) { + if ( $args['ignore_accepted'] && AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS === $term->term_group ) { continue; } $errors[] = array( @@ -351,17 +351,17 @@ public static function filter_views_edit( $views ) { 'update_post_term_cache' => false, ); - $with_new_query = new WP_Query( array_merge( + $with_new_query = new WP_Query( array_merge( $args, array( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS ) ) ); - $with_acknowledged_query = new WP_Query( array_merge( + $with_rejected_query = new WP_Query( array_merge( $args, - array( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ) + array( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECTED_STATUS ) ) ); - $with_ignored_query = new WP_Query( array_merge( + $with_accepted_query = new WP_Query( array_merge( $args, - array( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS ) + array( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS ) ) ); $current_url = remove_query_arg( @@ -375,7 +375,7 @@ public static function filter_views_edit( $views ) { $current_status = null; if ( isset( $_GET[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR ] ) ) { // WPCS: CSRF ok. $value = intval( $_GET[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR ] ); // WPCS: CSRF ok. - if ( in_array( $value, array( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS, AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS, AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ), true ) ) { + if ( in_array( $value, 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 ), true ) ) { $current_status = $value; } } @@ -403,49 +403,49 @@ public static function filter_views_edit( $views ) { ) ); - $views['acknowledged'] = sprintf( + $views['rejected'] = sprintf( '%s', esc_url( add_query_arg( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR, - AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS, + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECTED_STATUS, $current_url ) ), - AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS === $current_status ? 'current' : '', + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECTED_STATUS === $current_status ? 'current' : '', sprintf( /* translators: %s is the post count */ _nx( - 'With Acknowledged Errors (%s)', - 'With Acknowledged Errors (%s)', - $with_acknowledged_query->found_posts, + 'With Rejected Errors (%s)', + 'With Rejected Errors (%s)', + $with_rejected_query->found_posts, 'posts', 'amp' ), - number_format_i18n( $with_acknowledged_query->found_posts ) + number_format_i18n( $with_rejected_query->found_posts ) ) ); - $views['ignored'] = sprintf( + $views['accepted'] = sprintf( '%s', esc_url( add_query_arg( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR, - AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS, + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS, $current_url ) ), - AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS === $current_status ? 'current' : '', + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS === $current_status ? 'current' : '', sprintf( /* translators: %s is the post count */ _nx( - 'With Ignored Errors (%s)', - 'With Ignored Errors (%s)', - $with_ignored_query->found_posts, + 'With Accepted Errors (%s)', + 'With Accepted Errors (%s)', + $with_accepted_query->found_posts, 'posts', 'amp' ), - number_format_i18n( $with_ignored_query->found_posts ) + number_format_i18n( $with_accepted_query->found_posts ) ) ); @@ -496,8 +496,8 @@ public static function output_custom_column( $column_name, $post_id ) { $counts = array_fill_keys( array( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS, - AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS, - AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS, + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS, + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECTED_STATUS, ), 0 ); @@ -522,18 +522,18 @@ public static function output_custom_column( $column_name, $post_id ) { number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS ] ) ) ); } - if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS ] ) { + if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS ] ) { $displayed_counts[] = esc_html( sprintf( /* translators: %s is count */ - __( 'Ignored: %s', 'amp' ), - number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS ] ) + __( 'Accepted: %s', 'amp' ), + number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS ] ) ) ); } - if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ] ) { + if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECTED_STATUS ] ) { $displayed_counts[] = esc_html( sprintf( /* translators: %s is count */ - __( 'Acknowledged: %s', 'amp' ), - number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ] ) + __( 'Rejected: %s', 'amp' ), + number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECTED_STATUS ] ) ) ); } echo implode( '
      ', $displayed_counts ); // WPCS: xss ok. @@ -777,8 +777,8 @@ public static function print_status_meta_box( $post ) { $counts = array_fill_keys( array( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS, - AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS, - AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS, + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS, + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECTED_STATUS, ), 0 ); @@ -800,18 +800,18 @@ public static function print_status_meta_box( $post ) { number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_STATUS ] ) ) ); } - if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS ] ) { + if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS ] ) { $displayed_counts[] = esc_html( sprintf( /* translators: %s is count */ - __( 'Ignored: %s', 'amp' ), - number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS ] ) + __( 'Accepted: %s', 'amp' ), + number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS ] ) ) ); } - if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ] ) { + if ( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECTED_STATUS ] ) { $displayed_counts[] = esc_html( sprintf( /* translators: %s is count */ - __( 'Acknowledged: %s', 'amp' ), - number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ] ) + __( 'Rejected: %s', 'amp' ), + number_format_i18n( $counts[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECTED_STATUS ] ) ) ); } @@ -871,42 +871,42 @@ public static function print_validation_errors_meta_box( $post ) { term_group ) : ?> - term_group ) : ?> - - term_group ) : ?> - + term_group ) : ?> + + term_group ) : ?> +

      term_group ) { - $actions[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = sprintf( + if ( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECTED_STATUS !== $term->term_group ) { + $actions[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECT_ACTION ] = sprintf( '%s', wp_nonce_url( add_query_arg( - array_merge( array( 'action' => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ), compact( 'term_id' ) ), + array_merge( array( 'action' => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECT_ACTION ), compact( 'term_id' ) ), $edit_terms_url ), - AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACKNOWLEDGE_ACTION + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECT_ACTION ), - esc_attr__( 'Acknowledging an error marks it as read. AMP validation errors prevent a URL from being served as AMP.', 'amp' ), - esc_html__( 'Acknowledge', 'amp' ) + esc_attr__( 'Rejecting an error marks it as read. AMP validation errors prevent a URL from being served as AMP.', 'amp' ), + esc_html__( 'Reject', 'amp' ) ); } - if ( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS !== $term->term_group ) { - $actions[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORE_ACTION ] = sprintf( + if ( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS !== $term->term_group ) { + $actions[ AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPT_ACTION ] = sprintf( '%s', wp_nonce_url( add_query_arg( - array_merge( array( 'action' => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORE_ACTION ), compact( 'term_id' ) ), + array_merge( array( 'action' => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPT_ACTION ), compact( 'term_id' ) ), $edit_terms_url ), - AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORE_ACTION + AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPT_ACTION ), - esc_attr__( 'Ignoring an error prevents it from blocking a URL from being served as AMP.', 'amp' ), - esc_html__( 'Ignore', 'amp' ) + esc_attr__( 'Accepting an error prevents it from blocking a URL from being served as AMP.', 'amp' ), + esc_html__( 'Accept', 'amp' ) ); } echo implode( ' | ', $actions ); // WPCS: xss ok. diff --git a/includes/validation/class-amp-validation-error-taxonomy.php b/includes/validation/class-amp-validation-error-taxonomy.php index 506a7e89d8c..6585d60095d 100644 --- a/includes/validation/class-amp-validation-error-taxonomy.php +++ b/includes/validation/class-amp-validation-error-taxonomy.php @@ -27,32 +27,32 @@ class AMP_Validation_Error_Taxonomy { const VALIDATION_ERROR_NEW_STATUS = 0; /** - * Term group for validation_error terms that the user acknowledges as being ignored (and thus not disabling AMP). + * Term group for validation_error terms that the accepts and thus can be sanitized and does not disable AMP. * * @var int */ - const VALIDATION_ERROR_IGNORED_STATUS = 1; + const VALIDATION_ERROR_ACCEPTED_STATUS = 1; /** - * Term group for validation_error terms that the user acknowledges (as being blockers to enabling AMP). + * Term group for validation_error terms that the user flags as being blockers to enabling AMP. * * @var int */ - const VALIDATION_ERROR_ACKNOWLEDGED_STATUS = 2; + const VALIDATION_ERROR_REJECTED_STATUS = 2; /** * Action name for ignoring a validation error. * * @var string */ - const VALIDATION_ERROR_IGNORE_ACTION = 'amp_validation_error_ignore'; + const VALIDATION_ERROR_ACCEPT_ACTION = 'amp_validation_error_accept'; /** - * Action name for acknowledging a validation error. + * Action name for rejecting a validation error. * * @var string */ - const VALIDATION_ERROR_ACKNOWLEDGE_ACTION = 'amp_validation_error_acknowledge'; + const VALIDATION_ERROR_REJECT_ACTION = 'amp_validation_error_reject'; /** * Query var used when filtering by validation error status. @@ -163,13 +163,13 @@ public static function register() { } /** - * Get the count of validation error terms, optionally restricted by term group (e.g. ignored or acknowledged). + * Get the count of validation error terms, optionally restricted by term group (e.g. accepted or rejected). * * @param array $args { * Args passed into wp_count_terms(). * * @type int|null $group Term group. - * @type bool $ignore_empty Ignore terms that are no longer associated with any URLs. Default false. + * @type bool $ignore_empty Accept terms that are no longer associated with any URLs. Default false. * } * @return int Term count. */ @@ -358,8 +358,8 @@ public static function add_admin_hooks() { // Add bulk actions. add_filter( 'bulk_actions-edit-' . self::TAXONOMY_SLUG, function( $bulk_actions ) { - $bulk_actions[ self::VALIDATION_ERROR_IGNORE_ACTION ] = __( 'Ignore', 'amp' ); - $bulk_actions[ self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = __( 'Acknowledge', 'amp' ); + $bulk_actions[ self::VALIDATION_ERROR_ACCEPT_ACTION ] = __( 'Accept', 'amp' ); + $bulk_actions[ self::VALIDATION_ERROR_REJECT_ACTION ] = __( 'Reject', 'amp' ); return $bulk_actions; } ); @@ -422,7 +422,7 @@ public static function add_group_terms_clauses_filter() { } self::$should_filter_terms_clauses_for_error_validation_status = true; $group = intval( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ); // WPCS: CSRF ok. - if ( ! in_array( $group, array( self::VALIDATION_ERROR_NEW_STATUS, self::VALIDATION_ERROR_IGNORED_STATUS, self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ), true ) ) { + if ( ! in_array( $group, array( self::VALIDATION_ERROR_NEW_STATUS, self::VALIDATION_ERROR_ACCEPTED_STATUS, self::VALIDATION_ERROR_REJECTED_STATUS ), true ) ) { return; } add_filter( 'terms_clauses', function( $clauses, $taxonomies ) use ( $group ) { @@ -464,23 +464,23 @@ public static function add_admin_notices() { $actioned = sanitize_key( $_GET['amp_actioned'] ); // WPCS: CSRF ok. $count = intval( $_GET['amp_actioned_count'] ); // WPCS: CSRF ok. $message = null; - if ( self::VALIDATION_ERROR_IGNORE_ACTION === $actioned ) { + if ( self::VALIDATION_ERROR_ACCEPT_ACTION === $actioned ) { $message = sprintf( - /* translators: %s is number of errors ignored */ + /* translators: %s is number of errors accepted */ _n( - 'Ignored %s error. It will no longer block related URLs from being served as AMP.', - 'Ignored %s errors. They will no longer block related URLs from being served as AMP.', + 'Accepted %s error. It will no longer block related URLs from being served as AMP.', + 'Accepted %s errors. They will no longer block related URLs from being served as AMP.', number_format_i18n( $count ), 'amp' ), $count ); - } elseif ( self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION === $actioned ) { + } elseif ( self::VALIDATION_ERROR_REJECT_ACTION === $actioned ) { $message = sprintf( - /* translators: %s is number of errors acknowledged */ + /* translators: %s is number of errors rejected */ _n( - 'Acknowledged %s error. It will continue to block related URLs from being served as AMP.', - 'Acknowledged %s errors. They will continue to block related URLs from being served as AMP.', + 'Rejected %s error. It will continue to block related URLs from being served as AMP.', + 'Rejected %s errors. They will continue to block related URLs from being served as AMP.', number_format_i18n( $count ), 'amp' ), @@ -514,26 +514,26 @@ public static function filter_tag_row_actions( $actions, WP_Term $tag ) { unset( $actions['delete'] ); } - if ( self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS !== $tag->term_group ) { - $actions[ self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ] = sprintf( + if ( self::VALIDATION_ERROR_REJECTED_STATUS !== $tag->term_group ) { + $actions[ self::VALIDATION_ERROR_REJECT_ACTION ] = sprintf( '%s', wp_nonce_url( - add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ), compact( 'term_id' ) ) ), - self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION + add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_REJECT_ACTION ), compact( 'term_id' ) ) ), + self::VALIDATION_ERROR_REJECT_ACTION ), - esc_attr__( 'Acknowledging an error marks it as read. AMP validation errors prevent a URL from being served as AMP.', 'amp' ), - esc_html__( 'Acknowledge', 'amp' ) + 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_IGNORED_STATUS !== $tag->term_group ) { - $actions[ self::VALIDATION_ERROR_IGNORE_ACTION ] = sprintf( + if ( self::VALIDATION_ERROR_ACCEPTED_STATUS !== $tag->term_group ) { + $actions[ self::VALIDATION_ERROR_ACCEPT_ACTION ] = sprintf( '%s', wp_nonce_url( - add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_IGNORE_ACTION ), compact( 'term_id' ) ) ), - self::VALIDATION_ERROR_IGNORE_ACTION + add_query_arg( array_merge( array( 'action' => self::VALIDATION_ERROR_ACCEPT_ACTION ), compact( 'term_id' ) ) ), + self::VALIDATION_ERROR_ACCEPT_ACTION ), - esc_attr__( 'Ignoring an error prevents it from blocking a URL from being served as AMP.', 'amp' ), - esc_html__( 'Ignore', 'amp' ) + 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' ) ); } } @@ -570,10 +570,10 @@ public static function add_admin_menu_validation_error_item() { * @return array Views. */ public static function filter_views_edit( $views ) { - $total_term_count = self::get_validation_error_count(); - $acknowledged_term_count = self::get_validation_error_count( array( 'group' => self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ) ); - $ignored_term_count = self::get_validation_error_count( array( 'group' => self::VALIDATION_ERROR_IGNORED_STATUS ) ); - $new_term_count = $total_term_count - $acknowledged_term_count - $ignored_term_count; + $total_term_count = self::get_validation_error_count(); + $rejected_term_count = self::get_validation_error_count( array( 'group' => self::VALIDATION_ERROR_REJECTED_STATUS ) ); + $accepted_term_count = self::get_validation_error_count( array( 'group' => self::VALIDATION_ERROR_ACCEPTED_STATUS ) ); + $new_term_count = $total_term_count - $rejected_term_count - $accepted_term_count; $current_url = remove_query_arg( array_merge( @@ -586,7 +586,7 @@ public static function filter_views_edit( $views ) { $current_status = null; if ( isset( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ) ) { // WPCS: CSRF ok. $value = intval( $_GET[ self::VALIDATION_ERROR_STATUS_QUERY_VAR ] ); // WPCS: CSRF ok. - if ( in_array( $value, array( self::VALIDATION_ERROR_NEW_STATUS, self::VALIDATION_ERROR_IGNORED_STATUS, self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS ), true ) ) { + if ( in_array( $value, array( self::VALIDATION_ERROR_NEW_STATUS, self::VALIDATION_ERROR_ACCEPTED_STATUS, self::VALIDATION_ERROR_REJECTED_STATUS ), true ) ) { $current_status = $value; } } @@ -631,49 +631,49 @@ public static function filter_views_edit( $views ) { ) ); - $views['acknowledged'] = sprintf( + $views['rejected'] = sprintf( '%s', esc_url( add_query_arg( self::VALIDATION_ERROR_STATUS_QUERY_VAR, - self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS, + self::VALIDATION_ERROR_REJECTED_STATUS, $current_url ) ), - self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS === $current_status ? 'current' : '', + self::VALIDATION_ERROR_REJECTED_STATUS === $current_status ? 'current' : '', sprintf( /* translators: %s is the term count */ _nx( - 'Acknowledged (%s)', - 'Acknowledged (%s)', - $acknowledged_term_count, + 'Rejected (%s)', + 'Rejected (%s)', + $rejected_term_count, 'terms', 'amp' ), - number_format_i18n( $acknowledged_term_count ) + number_format_i18n( $rejected_term_count ) ) ); - $views['ignored'] = sprintf( + $views['accepted'] = sprintf( '%s', esc_url( add_query_arg( self::VALIDATION_ERROR_STATUS_QUERY_VAR, - self::VALIDATION_ERROR_IGNORED_STATUS, + self::VALIDATION_ERROR_ACCEPTED_STATUS, $current_url ) ), - self::VALIDATION_ERROR_IGNORED_STATUS === $current_status ? 'current' : '', + self::VALIDATION_ERROR_ACCEPTED_STATUS === $current_status ? 'current' : '', sprintf( /* translators: %s is the term count */ _nx( - 'Ignored (%s)', - 'Ignored (%s)', - $ignored_term_count, + 'Accepted (%s)', + 'Accepted (%s)', + $accepted_term_count, 'terms', 'amp' ), - number_format_i18n( $ignored_term_count ) + number_format_i18n( $accepted_term_count ) ) ); return $views; @@ -709,10 +709,10 @@ public static function filter_manage_custom_columns( $content, $column_name, $te } break; case 'status': - if ( self::VALIDATION_ERROR_IGNORED_STATUS === $term->term_group ) { - $content = esc_html__( 'Ignored', 'amp' ); - } elseif ( self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS === $term->term_group ) { - $content = esc_html__( 'Acknowledged', 'amp' ); + if ( self::VALIDATION_ERROR_ACCEPTED_STATUS === $term->term_group ) { + $content = esc_html__( 'Accepted', 'amp' ); + } elseif ( self::VALIDATION_ERROR_REJECTED_STATUS === $term->term_group ) { + $content = esc_html__( 'Rejected', 'amp' ); } else { $content = esc_html__( 'New', 'amp' ); } @@ -866,10 +866,10 @@ public static function handle_inline_edit_request() { */ public static function handle_validation_error_update( $redirect_to, $action, $term_ids ) { $term_group = null; - if ( self::VALIDATION_ERROR_IGNORE_ACTION === $action ) { - $term_group = self::VALIDATION_ERROR_IGNORED_STATUS; - } elseif ( self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION === $action ) { - $term_group = self::VALIDATION_ERROR_ACKNOWLEDGED_STATUS; + if ( self::VALIDATION_ERROR_ACCEPT_ACTION === $action ) { + $term_group = self::VALIDATION_ERROR_ACCEPTED_STATUS; + } elseif ( self::VALIDATION_ERROR_REJECT_ACTION === $action ) { + $term_group = self::VALIDATION_ERROR_REJECTED_STATUS; } if ( $term_group ) { diff --git a/includes/validation/class-amp-validation-manager.php b/includes/validation/class-amp-validation-manager.php index c42117a2436..834147aff64 100644 --- a/includes/validation/class-amp-validation-manager.php +++ b/includes/validation/class-amp-validation-manager.php @@ -448,7 +448,7 @@ public static function add_validation_error( array $error, array $data = array() $slug = md5( wp_json_encode( $error ) ); $term = get_term_by( 'slug', $slug, AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ); - if ( ! self::$debug && ! empty( $term ) && AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_IGNORED_STATUS === $term->term_group ) { + if ( ! self::$debug && ! empty( $term ) && AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS === $term->term_group ) { $sanitized = true; } else { $sanitized = false; From 71d36e77f8c5d477dfcda534aca4489e0a2ce954 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 24 May 2018 22:34:45 -0700 Subject: [PATCH 32/55] Add iconography and success/warning messages for rejected/accepted states --- .../class-amp-invalid-url-post-type.php | 21 ++++++++++++++++--- .../class-amp-validation-error-taxonomy.php | 6 +++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/includes/validation/class-amp-invalid-url-post-type.php b/includes/validation/class-amp-invalid-url-post-type.php index ac9d31bfb70..a6d1b845216 100644 --- a/includes/validation/class-amp-invalid-url-post-type.php +++ b/includes/validation/class-amp-invalid-url-post-type.php @@ -847,6 +847,10 @@ public static function print_status_meta_box( $post ) { */ public static function print_validation_errors_meta_box( $post ) { $validation_errors = self::get_invalid_url_validation_errors( $post ); + + $can_serve_amp = 0 === count( array_filter( $validation_errors, function( $validation_error ) { + return AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS !== $validation_error['term']->term_group; + } ) ); ?> + + +

      +

      +
      + +
      +

      +
      + +
        @@ -870,11 +885,11 @@ public static function print_validation_errors_meta_box( $post ) {
        term_group ) ? 'open' : ''; ?>> term_group ) : ?> - + ❓ term_group ) : ?> - + ❌ term_group ) : ?> - + ✅ diff --git a/includes/validation/class-amp-validation-error-taxonomy.php b/includes/validation/class-amp-validation-error-taxonomy.php index 6585d60095d..6ba3269087c 100644 --- a/includes/validation/class-amp-validation-error-taxonomy.php +++ b/includes/validation/class-amp-validation-error-taxonomy.php @@ -710,11 +710,11 @@ public static function filter_manage_custom_columns( $content, $column_name, $te break; case 'status': if ( self::VALIDATION_ERROR_ACCEPTED_STATUS === $term->term_group ) { - $content = esc_html__( 'Accepted', 'amp' ); + $content = '✅ ' . esc_html__( 'Accepted', 'amp' ); } elseif ( self::VALIDATION_ERROR_REJECTED_STATUS === $term->term_group ) { - $content = esc_html__( 'Rejected', 'amp' ); + $content = '❌ ' . esc_html__( 'Rejected', 'amp' ); } else { - $content = esc_html__( 'New', 'amp' ); + $content = '❓ ' . esc_html__( 'New', 'amp' ); } break; case 'created_date_gmt': From 4cf66ba52cf826fa096cc0f7eaae2b43ed00e4ea Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 24 May 2018 22:51:06 -0700 Subject: [PATCH 33/55] Exclude webpack.config.js from build --- Gruntfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 33ba9d730a6..5eb1b60cb05 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -100,7 +100,7 @@ module.exports = function( grunt ) { versionAppend = commitHash + '-' + new Date().toISOString().replace( /\.\d+/, '' ).replace( /-|:/g, '' ); paths = lsOutput.trim().split( /\n/ ).filter( function( file ) { - return ! /^(blocks|\.|bin|([^/]+)+\.(md|json|xml)|Gruntfile\.js|tests|wp-assets|dev-lib|readme\.md|composer\..*)/.test( file ); + return ! /^(blocks|\.|bin|([^/]+)+\.(md|json|xml)|Gruntfile\.js|tests|wp-assets|dev-lib|readme\.md|composer\..*|webpack.*)/.test( file ); } ); paths.push( 'vendor/autoload.php' ); paths.push( 'assets/js/*-compiled.js' ); From 9dde3226a5998cab2ba12983d63f0fa8111fb338 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 24 May 2018 23:31:58 -0700 Subject: [PATCH 34/55] Fix excluding accepted validatione errors when showing notice in editor --- assets/js/amp-block-validation.js | 2 +- includes/validation/class-amp-validation-manager.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/js/amp-block-validation.js b/assets/js/amp-block-validation.js index fcb822402f4..430fc92524e 100644 --- a/assets/js/amp-block-validation.js +++ b/assets/js/amp-block-validation.js @@ -172,7 +172,7 @@ var ampBlockValidation = ( function() { // eslint-disable-line no-unused-vars ); } - noticeMessage += ' ' + wp.i18n.__( 'Non-ignored validation errors prevent AMP from being served.', 'amp' ); + noticeMessage += ' ' + wp.i18n.__( 'Non-accepted validation errors prevent AMP from being served.', 'amp' ); noticeElement = wp.element.createElement( 'p', {}, [ noticeMessage + ' ', ampValidity.review_link && wp.element.createElement( diff --git a/includes/validation/class-amp-validation-manager.php b/includes/validation/class-amp-validation-manager.php index 834147aff64..45dd3301e9b 100644 --- a/includes/validation/class-amp-validation-manager.php +++ b/includes/validation/class-amp-validation-manager.php @@ -339,7 +339,7 @@ public static function get_amp_validity_rest_field( $post_data, $field_name, $re array( 'review_link' => get_edit_post_link( $validation_status_post->ID, 'raw' ), 'errors' => wp_list_pluck( - AMP_Invalid_URL_Post_Type::get_invalid_url_validation_errors( $validation_status_post, array( 'ignore_ignored' => true ) ), + AMP_Invalid_URL_Post_Type::get_invalid_url_validation_errors( $validation_status_post, array( 'ignore_accepted' => true ) ), 'data' ), ) @@ -522,7 +522,7 @@ public static function print_edit_form_validation_status( $post ) { } $validation_errors = wp_list_pluck( - AMP_Invalid_URL_Post_Type::get_invalid_url_validation_errors( $invalid_url_post, array( 'ignore_ignored' => true ) ), + AMP_Invalid_URL_Post_Type::get_invalid_url_validation_errors( $invalid_url_post, array( 'ignore_accepted' => true ) ), 'data' ); @@ -533,7 +533,7 @@ public static function print_edit_form_validation_status( $post ) { echo '
        '; echo '

        '; - esc_html_e( 'There is content which fails AMP validation. Non-ignored validation errors prevent AMP from being served.', 'amp' ); + esc_html_e( 'There is content which fails AMP validation. Non-accepted validation errors prevent AMP from being served.', 'amp' ); echo sprintf( ' %s', esc_url( get_edit_post_link( $invalid_url_post ) ), From 9a79f0559fdbc6ee036e2eb77f0060041c4c5477 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 25 May 2018 10:55:35 -0700 Subject: [PATCH 35/55] Remove debug links since not helpful; let view links include param to prevent auto-redirect to non-AMP version --- assets/js/amp-block-validation.js | 6 ------ .../class-amp-invalid-url-post-type.php | 19 +++++-------------- .../class-amp-validation-manager.php | 7 ------- 3 files changed, 5 insertions(+), 27 deletions(-) diff --git a/assets/js/amp-block-validation.js b/assets/js/amp-block-validation.js index 430fc92524e..c598c27a1b7 100644 --- a/assets/js/amp-block-validation.js +++ b/assets/js/amp-block-validation.js @@ -179,12 +179,6 @@ var ampBlockValidation = ( function() { // eslint-disable-line no-unused-vars 'a', { key: 'review_link', href: ampValidity.review_link, target: '_blank' }, wp.i18n.__( 'Review issues', 'amp' ) - ), - ampValidity.review_link && ' | ', - ampValidity.debug_link && wp.element.createElement( - 'a', - { key: 'debug_link', href: ampValidity.debug_link, target: '_blank' }, - wp.i18n.__( 'Debug', 'amp' ) ) ] ); diff --git a/includes/validation/class-amp-invalid-url-post-type.php b/includes/validation/class-amp-invalid-url-post-type.php index a6d1b845216..32d6e3faf8b 100644 --- a/includes/validation/class-amp-invalid-url-post-type.php +++ b/includes/validation/class-amp-invalid-url-post-type.php @@ -604,15 +604,11 @@ public static function filter_row_actions( $actions, $post ) { unset( $actions['inline hide-if-no-js'] ); $url = $post->post_title; + $view_url = add_query_arg( AMP_Validation_Manager::VALIDATE_QUERY_VAR, '', $url ); // Prevent redirection to non-AMP page. + $actions['view'] = sprintf( '%s', esc_url( $view_url ), esc_html__( 'View', 'amp' ) ); + if ( ! empty( $url ) ) { $actions[ self::RECHECK_ACTION ] = self::get_recheck_link( $post, get_edit_post_link( $post->ID, 'raw' ), $url ); - - $actions[ AMP_Validation_Manager::DEBUG_QUERY_VAR ] = sprintf( - '%s', - esc_url( AMP_Validation_Manager::get_debug_url( $url ) ), - esc_attr__( 'Validate URL on frontend but without invalid elements/attributes removed', 'amp' ), - esc_html__( 'Debug', 'amp' ) - ); } return $actions; @@ -823,14 +819,9 @@ public static function print_status_meta_box( $post ) { $url = $post->post_title; echo '

        '; - printf( '%s | ', esc_url( $url ), esc_html__( 'View', 'amp' ) ); + $view_url = add_query_arg( AMP_Validation_Manager::VALIDATE_QUERY_VAR, '', $url ); // Prevent redirection to non-AMP page. + printf( '%s | ', esc_url( $view_url ), esc_html__( 'View', 'amp' ) ); echo self::get_recheck_link( $post, $redirect_url ); // WPCS: XSS ok. - printf( - ' | %s', - esc_url( AMP_Validation_Manager::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 '
        '; diff --git a/includes/validation/class-amp-validation-manager.php b/includes/validation/class-amp-validation-manager.php index 45dd3301e9b..48d2821d133 100644 --- a/includes/validation/class-amp-validation-manager.php +++ b/includes/validation/class-amp-validation-manager.php @@ -539,13 +539,6 @@ public static function print_edit_form_validation_status( $post ) { esc_url( get_edit_post_link( $invalid_url_post ) ), esc_html__( 'Review issues', 'amp' ) ); - echo ' | '; - echo sprintf( - ' %s', - esc_url( self::get_debug_url( $amp_url ) ), - esc_attr__( 'Validate URL on frontend but without invalid elements/attributes removed', 'amp' ), - esc_html__( 'Debug', 'amp' ) - ); echo '

        '; $results = AMP_Validation_Error_Taxonomy::summarize_validation_errors( array_unique( $validation_errors, SORT_REGULAR ) ); From 932ec66b40d2387bfa93b8ae5c63c57977b863ed Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 26 May 2018 09:00:08 -0700 Subject: [PATCH 36/55] Vary parsed stylesheet cache by the sanitization status for its validation errors --- includes/class-amp-theme-support.php | 2 +- .../sanitizers/class-amp-style-sanitizer.php | 211 ++++++++++-------- .../class-amp-validation-manager.php | 10 +- tests/test-class-amp-validation-utils.php | 8 +- 4 files changed, 126 insertions(+), 105 deletions(-) diff --git a/includes/class-amp-theme-support.php b/includes/class-amp-theme-support.php index b777534d9c7..7b06144d551 100644 --- a/includes/class-amp-theme-support.php +++ b/includes/class-amp-theme-support.php @@ -159,7 +159,7 @@ public static function finish_init() { self::add_hooks(); self::$sanitizer_classes = amp_get_content_sanitizers(); - self::$sanitizer_classes = AMP_Validation_Manager::add_validation_callback( self::$sanitizer_classes ); + self::$sanitizer_classes = AMP_Validation_Manager::filter_sanitizer_args( self::$sanitizer_classes ); self::$embed_handlers = self::register_content_embed_handlers(); } diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 3c1a061f0d1..f58b2a84fa1 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -582,37 +582,47 @@ private function process_link_element( DOMElement $element ) { * @return array Processed stylesheet parts. */ private function process_stylesheet( $stylesheet, $options = array() ) { - $cache_impacting_options = array_merge( - wp_array_slice_assoc( + $parsed = null; + $cache_key = null; + $cache_group = 'amp-parsed-stylesheet-v5'; + $can_cache = ! $this->args['locate_sources']; + + if ( $can_cache ) { + $cache_impacting_options = wp_array_slice_assoc( $options, array( 'property_whitelist', 'property_blacklist', 'stylesheet_url', 'allowed_at_rules' ) - ), - array( - 'locate_sources' => ! empty( $this->args['locate_sources'] ), - ) - ); + ); - $cache_key = md5( $stylesheet . serialize( $cache_impacting_options ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + $cache_key = md5( $stylesheet . serialize( $cache_impacting_options ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize - $cache_group = 'amp-parsed-stylesheet-v4'; - if ( wp_using_ext_object_cache() ) { - $parsed = wp_cache_get( $cache_key, $cache_group ); - } else { - $parsed = get_transient( $cache_key . $cache_group ); - } - if ( ! $parsed || ! isset( $parsed['stylesheet'] ) || ! is_array( $parsed['stylesheet'] ) ) { - $parsed = $this->parse_stylesheet( $stylesheet, $options ); if ( wp_using_ext_object_cache() ) { - wp_cache_set( $cache_key, $parsed, $cache_group ); + $parsed = wp_cache_get( $cache_key, $cache_group ); } else { - // The expiration is to ensure transient doesn't stick around forever since no LRU flushing like with external object cache. - set_transient( $cache_key . $cache_group, $parsed, MONTH_IN_SECONDS ); + $parsed = get_transient( $cache_key . $cache_group ); + } + } + + // Make sure that the parsed stylesheet was cached with current sanitizations. + if ( ! empty( $parsed['validation_results'] ) ) { + foreach ( $parsed['validation_results'] as $validation_result ) { + $sanitized = $this->should_sanitize_validation_error( $validation_result['error'] ); + if ( $sanitized !== $validation_result['sanitized'] ) { + $parsed = null; // Change to sanitization of validation error detected, so cache cannot be used. + break; + } } - } elseif ( ! empty( $parsed['validation_errors'] ) ) { + } - // Report cached validation errors. - foreach ( $parsed['validation_errors'] as $validation_error ) { - $this->should_sanitize_validation_error( $validation_error ); // @todo Cannot affect result. + if ( ! $parsed || ! isset( $parsed['stylesheet'] ) || ! is_array( $parsed['stylesheet'] ) ) { + $parsed = $this->parse_stylesheet( $stylesheet, $options ); + + if ( $can_cache ) { + if ( wp_using_ext_object_cache() ) { + wp_cache_set( $cache_key, $parsed, $cache_group ); + } else { + // The expiration is to ensure transient doesn't stick around forever since no LRU flushing like with external object cache. + set_transient( $cache_key . $cache_group, $parsed, MONTH_IN_SECONDS ); + } } } @@ -629,8 +639,8 @@ private function process_stylesheet( $stylesheet, $options = array() ) { * @return array { * Parsed stylesheet. * - * @type array $stylesheet Stylesheet parts, where arrays are tuples for declaration blocks. - * @type array $validation_errors Validation errors. + * @type array $stylesheet Stylesheet parts, where arrays are tuples for declaration blocks. + * @type array $validation_results Validation results, array containing arrays with error and sanitized keys. * } */ private function parse_stylesheet( $stylesheet_string, $options = array() ) { @@ -655,8 +665,8 @@ private function parse_stylesheet( $stylesheet_string, $options = array() ) { // Find calc() functions and replace with placeholders since PHP-CSS-Parser can't handle them. $stylesheet_string = $this->add_calc_placeholders( $stylesheet_string ); - $stylesheet = array(); - $validation_errors = array(); + $stylesheet = array(); + $validation_results = array(); try { $parser_settings = Sabberworm\CSS\Settings::create(); $css_parser = new Sabberworm\CSS\Parser( $stylesheet_string, $parser_settings ); @@ -674,7 +684,7 @@ function ( $value ) { ); } - $validation_errors = array_merge( $validation_errors, $this->process_css_list( $css_document, $options ) ); + $validation_results = array_merge( $validation_results, $this->process_css_list( $css_document, $options ) ); $output_format = Sabberworm\CSS\OutputFormat::createCompact(); $output_format->setSemicolonAfterLastRule( false ); @@ -759,17 +769,23 @@ function( $matches ) use ( $selector, &$selectors_parsed ) { // Reset calc placeholders. $this->calc_placeholders = array(); } catch ( Exception $exception ) { - $validation_error = array( + $error = array( 'code' => 'css_parse_error', 'message' => $exception->getMessage(), ); - $this->should_sanitize_validation_error( $validation_error ); - $validation_error[] = $validation_error; + + /* + * This is not a recoverable error, so sanitized here is just used to give user control + * over whether to proceed with serving this exception-raising stylesheet in AMP. + */ + $sanitized = $this->should_sanitize_validation_error( $error ); + + $validation_results[] = compact( 'error', 'sanitized' ); } $this->parse_css_duration += ( microtime( true ) - $start_time ); - return compact( 'stylesheet', 'validation_errors' ); + return compact( 'stylesheet', 'validation_results' ); } /** @@ -849,90 +865,90 @@ private function add_calc_placeholders( $css ) { * @return array Validation errors. */ private function process_css_list( CSSList $css_list, $options ) { - $validation_errors = array(); + $results = array(); foreach ( $css_list->getContents() as $css_item ) { - $should_remove_item = false; + $sanitized = false; if ( $css_item instanceof DeclarationBlock && empty( $options['validate_keyframes'] ) ) { - $validation_errors = array_merge( - $validation_errors, + $results = array_merge( + $results, $this->process_css_declaration_block( $css_item, $css_list, $options ) ); } elseif ( $css_item instanceof AtRuleBlockList ) { if ( ! in_array( $css_item->atRuleName(), $options['allowed_at_rules'], true ) ) { - $validation_error = array( + $error = array( 'code' => 'illegal_css_at_rule', 'at_rule' => $css_item->atRuleName(), ); - $validation_errors[] = $validation_error; - $should_remove_item = $this->should_sanitize_validation_error( $validation_error ); + $sanitized = $this->should_sanitize_validation_error( $error ); + $results[] = compact( 'error', 'sanitized' ); } - if ( ! $should_remove_item ) { - $validation_errors = array_merge( - $validation_errors, + if ( ! $sanitized ) { + $results = array_merge( + $results, $this->process_css_list( $css_item, $options ) ); } } elseif ( $css_item instanceof Import ) { - $validation_error = array( + $error = array( 'code' => 'illegal_css_at_rule', 'at_rule' => 'import', ); - $validation_errors[] = $validation_error; - $should_remove_item = $this->should_sanitize_validation_error( $validation_error ); + $sanitized = $this->should_sanitize_validation_error( $error ); + $results[] = compact( 'error', 'sanitized' ); } elseif ( $css_item instanceof AtRuleSet ) { if ( ! in_array( $css_item->atRuleName(), $options['allowed_at_rules'], true ) ) { - $validation_error = array( + $error = array( 'code' => 'illegal_css_at_rule', 'at_rule' => $css_item->atRuleName(), ); - $validation_errors[] = $validation_error; - $should_remove_item = $this->should_sanitize_validation_error( $validation_error ); + $sanitized = $this->should_sanitize_validation_error( $error ); + $results[] = compact( 'error', 'sanitized' ); } - if ( ! $should_remove_item ) { - $validation_errors = array_merge( - $validation_errors, + if ( ! $sanitized ) { + $results = array_merge( + $results, $this->process_css_declaration_block( $css_item, $css_list, $options ) ); } } elseif ( $css_item instanceof KeyFrame ) { if ( ! in_array( 'keyframes', $options['allowed_at_rules'], true ) ) { - $validation_error = array( + $error = array( 'code' => 'illegal_css_at_rule', 'at_rule' => $css_item->atRuleName(), ); - $validation_errors[] = $validation_error; - $should_remove_item = $this->should_sanitize_validation_error( $validation_error ); + $sanitized = $this->should_sanitize_validation_error( $error ); + $results[] = compact( 'error', 'sanitized' ); } - if ( ! $should_remove_item ) { - $validation_errors = array_merge( - $validation_errors, + if ( ! $sanitized ) { + $results = array_merge( + $results, $this->process_css_keyframes( $css_item, $options ) ); } } elseif ( $css_item instanceof AtRule ) { - $validation_error = array( + $error = array( 'code' => 'illegal_css_at_rule', 'at_rule' => $css_item->atRuleName(), ); - $validation_errors[] = $validation_error; - $should_remove_item = $this->should_sanitize_validation_error( $validation_error ); + $sanitized = $this->should_sanitize_validation_error( $error ); + $results[] = compact( 'error', 'sanitized' ); } else { - $validation_error = array( + $error = array( 'code' => 'unrecognized_css', 'item' => get_class( $css_item ), ); - $validation_errors[] = $validation_error; - $should_remove_item = $this->should_sanitize_validation_error( $validation_error ); + $sanitized = $this->should_sanitize_validation_error( $error ); + $results[] = compact( 'error', 'sanitized' ); } - if ( $should_remove_item ) { + if ( $sanitized ) { $css_list->remove( $css_item ); } } - return $validation_errors; + return $results; } /** @@ -979,10 +995,10 @@ private function real_path_urls( $urls, $stylesheet_url ) { * @param CSSList $css_list CSS List. * @param array $options Options. * - * @return array Validation errors. + * @return array Validation results. */ private function process_css_declaration_block( RuleSet $ruleset, CSSList $css_list, $options ) { - $validation_errors = array(); + $results = array(); // Remove disallowed properties. if ( ! empty( $options['property_whitelist'] ) ) { @@ -990,30 +1006,32 @@ private function process_css_declaration_block( RuleSet $ruleset, CSSList $css_l foreach ( $properties as $property ) { $vendorless_property_name = preg_replace( '/^-\w+-/', '', $property->getRule() ); if ( ! in_array( $vendorless_property_name, $options['property_whitelist'], true ) ) { - $validation_error = array( + $error = array( 'code' => 'illegal_css_property', 'property_name' => $property->getRule(), 'property_value' => $property->getValue(), ); - if ( $this->should_sanitize_validation_error( $validation_error ) ) { + $sanitized = $this->should_sanitize_validation_error( $error ); + if ( $sanitized ) { $ruleset->removeRule( $property->getRule() ); } - $validation_errors[] = $validation_error; + $results[] = compact( 'error', 'sanitized' ); } } } else { foreach ( $options['property_blacklist'] as $illegal_property_name ) { $properties = $ruleset->getRules( $illegal_property_name ); foreach ( $properties as $property ) { - $validation_error = array( + $error = array( 'code' => 'illegal_css_property', 'property_name' => $property->getRule(), 'property_value' => (string) $property->getValue(), ); - if ( $this->should_sanitize_validation_error( $validation_error ) ) { + $sanitized = $this->should_sanitize_validation_error( $error ); + if ( $sanitized ) { $ruleset->removeRule( $property->getRule() ); } - $validation_errors[] = $validation_error; + $results[] = compact( 'error', 'sanitized' ); } } } @@ -1022,8 +1040,8 @@ private function process_css_declaration_block( RuleSet $ruleset, CSSList $css_l $this->process_font_face_at_rule( $ruleset, $options ); } - $validation_errors = array_merge( - $validation_errors, + $results = array_merge( + $results, $this->transform_important_qualifiers( $ruleset, $css_list ) ); @@ -1032,7 +1050,7 @@ private function process_css_declaration_block( RuleSet $ruleset, CSSList $css_l $css_list->remove( $ruleset ); } // @todo Delete rules with selectors for -amphtml- class and i-amphtml- tags. - return $validation_errors; + return $results; } /** @@ -1157,26 +1175,27 @@ private function process_font_face_at_rule( AtRuleSet $ruleset, $options ) { * * @param KeyFrame $css_list Ruleset. * @param array $options Options. - * @return array Validation errors. + * @return array Validation results. */ private function process_css_keyframes( KeyFrame $css_list, $options ) { - $validation_errors = array(); + $results = array(); if ( ! empty( $options['property_whitelist'] ) ) { foreach ( $css_list->getContents() as $rules ) { if ( ! ( $rules instanceof DeclarationBlock ) ) { - $validation_error = array( + $error = array( 'code' => 'unrecognized_css', 'item' => get_class( $rules ), ); - if ( $this->should_sanitize_validation_error( $validation_error ) ) { + $sanitized = $this->should_sanitize_validation_error( $error ); + if ( $sanitized ) { $css_list->remove( $rules ); } - $validation_errors[] = $validation_error; + $results[] = compact( 'error', 'sanitized' ); continue; } - $validation_errors = array_merge( - $validation_errors, + $results = array_merge( + $results, $this->transform_important_qualifiers( $rules, $css_list ) ); @@ -1184,20 +1203,21 @@ private function process_css_keyframes( KeyFrame $css_list, $options ) { foreach ( $properties as $property ) { $vendorless_property_name = preg_replace( '/^-\w+-/', '', $property->getRule() ); if ( ! in_array( $vendorless_property_name, $options['property_whitelist'], true ) ) { - $validation_error = array( + $error = array( 'code' => 'illegal_css_property', 'property_name' => $property->getRule(), 'property_value' => (string) $property->getValue(), ); - if ( $this->should_sanitize_validation_error( $validation_error ) ) { + $sanitized = $this->should_sanitize_validation_error( $error ); + if ( $sanitized ) { $rules->removeRule( $property->getRule() ); } - $validation_errors[] = $validation_error; + $results[] = compact( 'error', 'sanitized' ); } } } } - return $validation_errors; + return $results; } /** @@ -1209,10 +1229,10 @@ private function process_css_keyframes( KeyFrame $css_list, $options ) { * * @param RuleSet|DeclarationBlock $ruleset Rule set. * @param CSSList $css_list CSS List. - * @return array Validation errors. + * @return array Validation results. */ private function transform_important_qualifiers( RuleSet $ruleset, CSSList $css_list ) { - $validation_errors = array(); + $results = array(); // An !important only makes sense for rulesets that have selectors. $allow_transformation = ( @@ -1230,18 +1250,19 @@ private function transform_important_qualifiers( RuleSet $ruleset, CSSList $css_ $property->setIsImportant( false ); $ruleset->removeRule( $property->getRule() ); } else { - $validation_error = array( + $error = array( 'code' => 'illegal_css_important', ); - if ( $this->should_sanitize_validation_error( $validation_error ) ) { + $sanitized = $this->should_sanitize_validation_error( $error ); + if ( $sanitized ) { $property->setIsImportant( false ); } - $validation_errors[] = $validation_error; + $results[] = compact( 'error', 'sanitized' ); } } } if ( ! $allow_transformation || empty( $importants ) ) { - return $validation_errors; + return $results; } $important_ruleset = clone $ruleset; @@ -1280,7 +1301,7 @@ function( Selector $old_selector ) { $important_ruleset->setRules( $importants ); $css_list->append( $important_ruleset ); // @todo It would be preferable if the important ruleset were inserted adjacent to the original rule. - return $validation_errors; + return $results; } /** @@ -1524,7 +1545,7 @@ function( $selector ) { $final_size = 0; $dom = $this->dom; - foreach ( $stylesheet_set['pending_stylesheets'] as &$pending_stylesheet ) { // Note: There are two elements in this array. + foreach ( $stylesheet_set['pending_stylesheets'] as &$pending_stylesheet ) { $stylesheet = ''; foreach ( $pending_stylesheet['stylesheet'] as $stylesheet_part ) { if ( is_string( $stylesheet_part ) ) { diff --git a/includes/validation/class-amp-validation-manager.php b/includes/validation/class-amp-validation-manager.php index 48d2821d133..034af29082b 100644 --- a/includes/validation/class-amp-validation-manager.php +++ b/includes/validation/class-amp-validation-manager.php @@ -1243,7 +1243,7 @@ public static function finalize_validation( DOMDocument $dom, $args = array() ) $errors = wp_list_pluck( self::$validation_results, 'error' ); $encoded = wp_json_encode( $errors, 128 /* JSON_PRETTY_PRINT */ ); $encoded = str_replace( '--', '\u002d\u002d', $encoded ); // Prevent "--" in strings from breaking out of HTML comments. - $comment = $dom->createComment( 'AMP_VALIDATION_ERRORS:' . $encoded . "\n" ); + $comment = $dom->createComment( 'AMP_VALIDATION_ERRORS:' . $encoded . "\n" ); // @todo Rename to AMP_VALIDATION_RESULTS and then include sanitized. $dom->documentElement->appendChild( $comment ); } } @@ -1254,14 +1254,14 @@ public static function finalize_validation( DOMDocument $dom, $args = array() ) * @param array $sanitizers The AMP sanitizers. * @return array $sanitizers The filtered AMP sanitizers. */ - public static function add_validation_callback( $sanitizers ) { + public static function filter_sanitizer_args( $sanitizers ) { foreach ( $sanitizers as $sanitizer => &$args ) { $args['validation_error_callback'] = __CLASS__ . '::add_validation_error'; } // @todo Pass this into all sanitizers? if ( isset( $sanitizers['AMP_Style_Sanitizer'] ) ) { - $sanitizers['AMP_Style_Sanitizer']['locate_sources'] = true; + $sanitizers['AMP_Style_Sanitizer']['locate_sources'] = self::$locate_sources; } return $sanitizers; @@ -1279,7 +1279,7 @@ public static function validate_after_plugin_activation() { } $validation_errors = self::validate_url( $url ); if ( is_array( $validation_errors ) && count( $validation_errors ) > 0 ) { - self::store_validation_errors( $validation_errors, $url ); + AMP_Invalid_URL_Post_Type::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 ); @@ -1358,7 +1358,7 @@ public static function plugin_notice() { '%s', esc_url( add_query_arg( 'post_type', - self::POST_TYPE_SLUG, + AMP_Invalid_URL_Post_Type::POST_TYPE_SLUG, admin_url( 'edit.php' ) ) ), __( 'More details', 'amp' ) diff --git a/tests/test-class-amp-validation-utils.php b/tests/test-class-amp-validation-utils.php index a40ca30f6b6..b20f1e1bc49 100644 --- a/tests/test-class-amp-validation-utils.php +++ b/tests/test-class-amp-validation-utils.php @@ -811,11 +811,11 @@ public function test_should_validate_response() { } /** - * Test add_validation_callback + * Test filter_sanitizer_args * - * @covers AMP_Validation_Manager::add_validation_callback() + * @covers AMP_Validation_Manager::filter_sanitizer_args() */ - public function test_add_validation_callback() { + public function test_filter_sanitizer_args() { global $post; $post = $this->factory()->post->create_and_get(); // WPCS: global override ok. $sanitizers = array( @@ -825,7 +825,7 @@ public function test_add_validation_callback() { ); $expected_callback = self::TESTED_CLASS . '::add_validation_error'; - $filtered_sanitizers = AMP_Validation_Manager::add_validation_callback( $sanitizers ); + $filtered_sanitizers = AMP_Validation_Manager::filter_sanitizer_args( $sanitizers ); foreach ( $filtered_sanitizers as $sanitizer => $args ) { $this->assertEquals( $expected_callback, $args['validation_error_callback'] ); } From 8a923550f0818624c2aae9061f8adf75c4d8c19d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 26 May 2018 10:19:58 -0700 Subject: [PATCH 37/55] Ensure node is made available to amp_validation_error filter from style sanitizer --- includes/sanitizers/class-amp-base-sanitizer.php | 4 ---- .../sanitizers/class-amp-style-sanitizer.php | 16 ++++++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/includes/sanitizers/class-amp-base-sanitizer.php b/includes/sanitizers/class-amp-base-sanitizer.php index c40f2512687..82bcc3e754c 100644 --- a/includes/sanitizers/class-amp-base-sanitizer.php +++ b/includes/sanitizers/class-amp-base-sanitizer.php @@ -329,14 +329,10 @@ public function remove_invalid_attribute( $element, $attribute, $validation_erro } /** - * Call the validation_error_callback. - * * Check whether or not sanitization should occur in response to validation error. * * @since 1.0 * - * @todo Each sanitizer needs a $locate_sources arg. - * * @param array $validation_error Validation error. * @param array $data Data including the node. * @return bool Whether to sanitize. diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index f58b2a84fa1..9c82ac4ffe1 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -789,20 +789,24 @@ function( $matches ) use ( $selector, &$selectors_parsed ) { } /** - * Prepare validation error. + * Check whether or not sanitization should occur in response to validation error. * - * @param array $error Error. - * @param array $data Data. - * @return array Error. + * Supply sources to the error and the current node to data. + * + * @since 1.0 + * + * @param array $validation_error Validation error. + * @param array $data Data including the node. + * @return bool Whether to sanitize. */ - public function prepare_validation_error( array $error = array(), array $data = array() ) { + public function should_sanitize_validation_error( $validation_error, $data = array() ) { if ( ! isset( $data['node'] ) ) { $data['node'] = $this->current_node; } if ( ! isset( $error['sources'] ) ) { $error['sources'] = $this->current_sources; } - return parent::prepare_validation_error( $error, $data ); + return parent::should_sanitize_validation_error( $validation_error, $data ); } /** From c58f897df80313487ab5f7358bddf1d59ee7e1f0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 26 May 2018 10:44:44 -0700 Subject: [PATCH 38/55] Let parsed stylesheet caching be done invariably --- .../sanitizers/class-amp-style-sanitizer.php | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 9c82ac4ffe1..734ae8d0d98 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -585,21 +585,24 @@ private function process_stylesheet( $stylesheet, $options = array() ) { $parsed = null; $cache_key = null; $cache_group = 'amp-parsed-stylesheet-v5'; - $can_cache = ! $this->args['locate_sources']; - if ( $can_cache ) { - $cache_impacting_options = wp_array_slice_assoc( + $cache_impacting_options = array_merge( + wp_array_slice_assoc( $options, array( 'property_whitelist', 'property_blacklist', 'stylesheet_url', 'allowed_at_rules' ) - ); + ), + array( + 'locate_sources' => ! empty( $this->args['locate_sources'] ), + // @todo There will need to be a variant for preview, probably is_customize_preview() or $wp_customize->changeset_uuid(). + ) + ); - $cache_key = md5( $stylesheet . serialize( $cache_impacting_options ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + $cache_key = md5( $stylesheet . serialize( $cache_impacting_options ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize - if ( wp_using_ext_object_cache() ) { - $parsed = wp_cache_get( $cache_key, $cache_group ); - } else { - $parsed = get_transient( $cache_key . $cache_group ); - } + if ( wp_using_ext_object_cache() ) { + $parsed = wp_cache_get( $cache_key, $cache_group ); + } else { + $parsed = get_transient( $cache_key . $cache_group ); } // Make sure that the parsed stylesheet was cached with current sanitizations. @@ -616,13 +619,11 @@ private function process_stylesheet( $stylesheet, $options = array() ) { if ( ! $parsed || ! isset( $parsed['stylesheet'] ) || ! is_array( $parsed['stylesheet'] ) ) { $parsed = $this->parse_stylesheet( $stylesheet, $options ); - if ( $can_cache ) { - if ( wp_using_ext_object_cache() ) { - wp_cache_set( $cache_key, $parsed, $cache_group ); - } else { - // The expiration is to ensure transient doesn't stick around forever since no LRU flushing like with external object cache. - set_transient( $cache_key . $cache_group, $parsed, MONTH_IN_SECONDS ); - } + if ( wp_using_ext_object_cache() ) { + wp_cache_set( $cache_key, $parsed, $cache_group ); + } else { + // The expiration is to ensure transient doesn't stick around forever since no LRU flushing like with external object cache. + set_transient( $cache_key . $cache_group, $parsed, MONTH_IN_SECONDS ); } } From e00e16d79660da9a8ed879097f7fb622611427d6 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 26 May 2018 11:27:03 -0700 Subject: [PATCH 39/55] Eliminate debug param in favor of explicitly requesting to preserve source comments * Introduce amp_preserve_source_comments query param. * Rename locate_sources arg to should_locate_sources. * Remove debug flag for preventing sanitization on all validation errors. * Rename add_validation_hooks method to add_validation_error_sourcing. --- includes/class-amp-theme-support.php | 8 +-- .../sanitizers/class-amp-base-sanitizer.php | 2 +- .../sanitizers/class-amp-style-sanitizer.php | 10 +-- .../class-amp-validation-manager.php | 64 ++++--------------- tests/test-class-amp-base-sanitizer.php | 4 +- tests/test-class-amp-theme-support.php | 1 - tests/test-class-amp-validation-utils.php | 27 +++----- 7 files changed, 34 insertions(+), 82 deletions(-) diff --git a/includes/class-amp-theme-support.php b/includes/class-amp-theme-support.php index 7b06144d551..2723c54e6ea 100644 --- a/includes/class-amp-theme-support.php +++ b/includes/class-amp-theme-support.php @@ -100,10 +100,8 @@ public static function init() { return; } - // @todo Rename to query var to indicate whether sources should be obtained. It's sources that are needing to be to be conditional. AMP_Validation_Manager::init( array( - 'locate_sources' => AMP_Validation_Manager::should_validate_response(), - 'debug' => isset( $_REQUEST[ AMP_Validation_Manager::DEBUG_QUERY_VAR ] ), // phpcs:ignore WordPress.CSRF.NonceVerification.NoNonceVerification + 'should_locate_sources' => AMP_Validation_Manager::should_validate_response(), ) ); self::$init_start_time = microtime( true ); @@ -1135,7 +1133,9 @@ public static function prepare_response( $response, $args = array() ) { } if ( AMP_Validation_Manager::should_validate_response() ) { - AMP_Validation_Manager::finalize_validation( $dom ); + AMP_Validation_Manager::finalize_validation( $dom, array( + 'remove_source_comments' => ! isset( $_GET['amp_preserve_source_comments'] ), // WPCS: CSRF. + ) ); } $response = "\n"; diff --git a/includes/sanitizers/class-amp-base-sanitizer.php b/includes/sanitizers/class-amp-base-sanitizer.php index 82bcc3e754c..5755c742528 100644 --- a/includes/sanitizers/class-amp-base-sanitizer.php +++ b/includes/sanitizers/class-amp-base-sanitizer.php @@ -54,7 +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 $locate_sources + * @type bool $should_locate_sources * @type callable $validation_error_callback * } */ diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 734ae8d0d98..896132862dd 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -35,7 +35,7 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer { * @type bool $require_https_src Require HTTPS URLs. * @type bool $allow_dirty_styles Allow dirty styles. This short-circuits the sanitize logic; it is used primarily in Customizer preview. * @type callable $validation_error_callback Function to call when a validation error is encountered. - * @type bool $locate_sources Whether to locate the sources when reporting validation errors. + * @type bool $should_locate_sources Whether to locate the sources when reporting validation errors. * } */ protected $args; @@ -53,7 +53,7 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer { '[submit-error]', '[submit-success]', ), - 'locate_sources' => false, + 'should_locate_sources' => false, ); /** @@ -451,7 +451,7 @@ private function set_current_node( $node ) { $this->current_node = $node; if ( empty( $node ) ) { $this->current_sources = null; - } elseif ( ! empty( $this->args['locate_sources'] ) ) { + } elseif ( ! empty( $this->args['should_locate_sources'] ) ) { $this->current_sources = AMP_Validation_Manager::locate_sources( $node ); } } @@ -592,8 +592,8 @@ private function process_stylesheet( $stylesheet, $options = array() ) { array( 'property_whitelist', 'property_blacklist', 'stylesheet_url', 'allowed_at_rules' ) ), array( - 'locate_sources' => ! empty( $this->args['locate_sources'] ), - // @todo There will need to be a variant for preview, probably is_customize_preview() or $wp_customize->changeset_uuid(). + 'should_locate_sources' => ! empty( $this->args['should_locate_sources'] ), + // @todo There will need to be a variant for preview, probably is_customize_preview() or $wp_customize->changeset_uuid(), or rather the list of sanitization overrides from query? ) ); diff --git a/includes/validation/class-amp-validation-manager.php b/includes/validation/class-amp-validation-manager.php index 034af29082b..9e7c49f6744 100644 --- a/includes/validation/class-amp-validation-manager.php +++ b/includes/validation/class-amp-validation-manager.php @@ -19,13 +19,6 @@ class AMP_Validation_Manager { */ const VALIDATE_QUERY_VAR = 'amp_validate'; - /** - * Query var that enables validation debug mode, to disable removal of invalid elements/attributes. - * - * @var string - */ - const DEBUG_QUERY_VAR = 'amp_debug'; - /** * Query var for cache-busting. * @@ -110,19 +103,9 @@ class AMP_Validation_Manager { /** * Whether validation error sources should be located. * - * @todo Rename to should_locate_sources - * @var bool - */ - public static $locate_sources = false; - - /** - * Whether in debug mode. - * - * This means that sanitization will not be applied for validation errors, and any source comments will not be removed. - * * @var bool */ - public static $debug = false; + public static $should_locate_sources = false; /** * Add the actions. @@ -130,21 +113,19 @@ class AMP_Validation_Manager { * @param array $args { * Args. * - * @type bool $debug Whether validation should be done in debug mode, where validation errors are not sanitized and source comments are not removed. + * @type bool $should_locate_sources Whether to locate sources. * } * @return void */ public static function init( $args = array() ) { $args = array_merge( array( - 'debug' => false, - 'locate_sources' => false, + 'should_locate_sources' => false, ), $args ); - self::$debug = $args['debug']; - self::$locate_sources = $args['locate_sources']; + 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' ) ); @@ -164,17 +145,15 @@ public static function init( $args = array() ) { } } ); - if ( self::$locate_sources ) { - self::add_validation_hooks(); + if ( self::$should_locate_sources ) { + self::add_validation_error_sourcing(); } } /** - * Add hooks for doing validation during preprocessing/sanitizing. - * - * @todo Rename to add_validation_error_source_tracing(). + * Add hooks for doing determining sources for validation errors during preprocessing/sanitizing. */ - public static function add_validation_hooks() { + public static function add_validation_error_sourcing() { add_action( 'wp', array( __CLASS__, 'wrap_widget_callbacks' ) ); add_action( 'all', array( __CLASS__, 'wrap_hook_callbacks' ) ); @@ -330,7 +309,6 @@ public static function get_amp_validity_rest_field( $post_data, $field_name, $re $field = array( 'errors' => array(), 'review_link' => null, - 'debug_link' => self::get_debug_url( amp_get_permalink( $post_data['id'] ) ), ); if ( $validation_status_post ) { @@ -407,7 +385,7 @@ public static function add_validation_error( array $error, array $data = array() $node = $data['node']; } - if ( self::$locate_sources ) { + if ( self::$should_locate_sources ) { if ( ! empty( $error['sources'] ) ) { $sources = $error['sources']; } elseif ( $node ) { @@ -448,7 +426,7 @@ public static function add_validation_error( array $error, array $data = array() $slug = md5( wp_json_encode( $error ) ); $term = get_term_by( 'slug', $slug, AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ); - if ( ! self::$debug && ! empty( $term ) && AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS === $term->term_group ) { + if ( ! empty( $term ) && AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS === $term->term_group ) { $sanitized = true; } else { $sanitized = false; @@ -601,6 +579,8 @@ public static function parse_source_comment( DOMComment $comment ) { /** * Walk back tree to find the open sources. * + * @todo This method and others for sourcing could be moved to a separate class. + * * @param DOMNode $node Node to look for. * @return array[][] { * The data of the removed sources (theme, plugin, or mu-plugin). @@ -1229,7 +1209,7 @@ public static function has_blocking_validation_errors() { public static function finalize_validation( DOMDocument $dom, $args = array() ) { $args = array_merge( array( - 'remove_source_comments' => ! self::$debug, + 'remove_source_comments' => true, 'append_validation_status_comment' => true, ), $args @@ -1261,7 +1241,7 @@ public static function filter_sanitizer_args( $sanitizers ) { // @todo Pass this into all sanitizers? if ( isset( $sanitizers['AMP_Style_Sanitizer'] ) ) { - $sanitizers['AMP_Style_Sanitizer']['locate_sources'] = self::$locate_sources; + $sanitizers['AMP_Style_Sanitizer']['should_locate_sources'] = self::$should_locate_sources; } return $sanitizers; @@ -1374,22 +1354,6 @@ public static function plugin_notice() { } } - /** - * Get validation debug UR:. - * - * @param string $url URL to to validate and debug. - * @return string Debug URL. - */ - public static function get_debug_url( $url ) { - return add_query_arg( - array( - self::VALIDATE_QUERY_VAR => '', - self::DEBUG_QUERY_VAR => '', - ), - $url - ) . '#development=1'; - } - /** * Enqueues the block validation script. * diff --git a/tests/test-class-amp-base-sanitizer.php b/tests/test-class-amp-base-sanitizer.php index 7ba35565257..c51236f749b 100644 --- a/tests/test-class-amp-base-sanitizer.php +++ b/tests/test-class-amp-base-sanitizer.php @@ -26,7 +26,7 @@ public function setUp() { public function tearDown() { parent::tearDown(); AMP_Validation_Manager::reset_validation_results(); - AMP_Validation_Manager::$locate_sources = false; + AMP_Validation_Manager::$should_locate_sources = false; } /** @@ -237,7 +237,7 @@ public function test_remove_child() { public function test_remove_attribute() { $this->markTestSkipped( 'Needs refactoring.' ); - AMP_Validation_Manager::$locate_sources = true; + AMP_Validation_Manager::$should_locate_sources = true; add_filter( 'amp_validation_error_sanitized', '__return_true' ); $video_name = 'amp-video'; $attribute = 'onload'; diff --git a/tests/test-class-amp-theme-support.php b/tests/test-class-amp-theme-support.php index 73efe37ee1c..ef3aa9455e9 100644 --- a/tests/test-class-amp-theme-support.php +++ b/tests/test-class-amp-theme-support.php @@ -26,7 +26,6 @@ class Test_AMP_Theme_Support extends WP_UnitTestCase { public function setUp() { parent::setUp(); AMP_Validation_Manager::reset_validation_results(); - AMP_Validation_Manager::$debug = false; unset( $GLOBALS['current_screen'] ); remove_theme_support( 'amp' ); } diff --git a/tests/test-class-amp-validation-utils.php b/tests/test-class-amp-validation-utils.php index b20f1e1bc49..a5c76312205 100644 --- a/tests/test-class-amp-validation-utils.php +++ b/tests/test-class-amp-validation-utils.php @@ -96,7 +96,7 @@ public function tearDown() { $GLOBALS['wp_registered_widgets'] = $this->original_wp_registered_widgets; // WPCS: override ok. remove_theme_support( 'amp' ); unset( $GLOBALS['current_screen'] ); - AMP_Validation_Manager::$locate_sources = false; + AMP_Validation_Manager::$should_locate_sources = false; parent::tearDown(); } @@ -128,10 +128,10 @@ public function test_init() { /** * Test add_validation_hooks. * - * @covers AMP_Validation_Manager::add_validation_hooks() + * @covers AMP_Validation_Manager::add_validation_error_sourcing() */ public function test_add_validation_hooks() { - AMP_Validation_Manager::add_validation_hooks(); + AMP_Validation_Manager::add_validation_error_sourcing(); $this->assertEquals( PHP_INT_MAX, has_filter( 'the_content', array( self::TESTED_CLASS, 'decorate_filter_source' ) ) ); $this->assertEquals( PHP_INT_MAX, has_filter( 'the_excerpt', array( self::TESTED_CLASS, 'decorate_filter_source' ) ) ); $this->assertEquals( -1, has_action( 'do_shortcode_tag', array( self::TESTED_CLASS, 'decorate_shortcode_source' ) ) ); @@ -140,7 +140,7 @@ public function test_add_validation_hooks() { /** * Test add_validation_hooks with Gutenberg active. * - * @covers AMP_Validation_Manager::add_validation_hooks() + * @covers AMP_Validation_Manager::add_validation_error_sourcing() */ public function test_add_validation_hooks_gutenberg() { if ( ! function_exists( 'do_blocks' ) ) { @@ -152,7 +152,7 @@ public function test_add_validation_hooks_gutenberg() { $priority = has_filter( 'the_content', 'do_blocks' ); $this->assertNotFalse( $priority ); - AMP_Validation_Manager::add_validation_hooks(); + AMP_Validation_Manager::add_validation_error_sourcing(); $this->assertEquals( $priority - 1, has_filter( 'the_content', array( self::TESTED_CLASS, 'add_block_source_comments' ) ) ); } @@ -253,7 +253,7 @@ public function test_add_block_source_comments( $content, $expected, $query ) { public function test_track_removed() { $this->markTestSkipped( 'Needs refactoring' ); - AMP_Validation_Manager::$locate_sources = true; + AMP_Validation_Manager::$should_locate_sources = true; $this->assertEmpty( AMP_Validation_Manager::$validation_results ); AMP_Validation_Manager::add_validation_error( array( @@ -556,7 +556,7 @@ public function test_callback_wrappers() { $action_two_arguments = 'example_action_two_arguments'; $notice = 'Example notice'; - AMP_Validation_Manager::add_validation_hooks(); + AMP_Validation_Manager::add_validation_error_sourcing(); add_action( $action_function_callback, '_amp_print_php_version_admin_notice' ); add_action( $action_no_argument, array( $this, 'output_div' ) ); @@ -663,7 +663,7 @@ public function test_callback_wrappers() { * @covers AMP_Validation_Manager::decorate_filter_source() */ public function test_decorate_shortcode_and_filter_source() { - AMP_Validation_Manager::add_validation_hooks(); + AMP_Validation_Manager::add_validation_error_sourcing(); add_shortcode( 'test', function() { return 'test'; } ); @@ -1438,17 +1438,6 @@ public function test_print_validation_errors_meta_box() { AMP_Validation_Manager::reset_validation_results(); } - /** - * Test for get_debug_url() - * - * @covers AMP_Validation_Manager::get_debug_url() - */ - public function test_get_debug_url() { - $this->assertContains( AMP_Validation_Manager::VALIDATE_QUERY_VAR, AMP_Validation_Manager::get_debug_url( 'https://example.com/foo/' ) ); - $this->assertContains( AMP_Validation_Manager::DEBUG_QUERY_VAR, AMP_Validation_Manager::get_debug_url( 'https://example.com/foo/' ) ); - $this->assertStringEndsWith( '#development=1', AMP_Validation_Manager::get_debug_url( 'https://example.com/foo/' ) ); - } - /** * Test for get_recheck_link() * From 6cd5e7ecda5c5f6206c964ebdf10e5e921b2ab19 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 26 May 2018 12:33:28 -0700 Subject: [PATCH 40/55] Include sanitization status with each error when getting validation results --- assets/js/amp-block-validation.js | 18 +++++++++-- .../class-amp-validation-manager.php | 32 ++++++++----------- tests/test-class-amp-validation-utils.php | 8 ++++- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/assets/js/amp-block-validation.js b/assets/js/amp-block-validation.js index c598c27a1b7..26cd6bd916e 100644 --- a/assets/js/amp-block-validation.js +++ b/assets/js/amp-block-validation.js @@ -104,7 +104,14 @@ var ampBlockValidation = ( function() { // eslint-disable-line no-unused-vars currentPost = wp.data.select( 'core/editor' ).getCurrentPost(); ampValidity = currentPost[ module.data.ampValidityRestField ] || {}; - validationErrors = ampValidity.errors; + validationErrors = _.map( + _.filter( ampValidity.results, function( result ) { + return ! result.sanitized; + } ), + function( result ) { + return result.error; + } + ); // Short-circuit if there was no change to the validation errors. if ( ! validationErrors || _.isEqual( module.lastValidationErrors, validationErrors ) ) { @@ -211,7 +218,14 @@ var ampBlockValidation = ( function() { // eslint-disable-line no-unused-vars var blockValidationErrorsByUid, editorSelect, currentPost, blockOrder, validationErrors, otherValidationErrors; editorSelect = wp.data.select( 'core/editor' ); currentPost = editorSelect.getCurrentPost(); - validationErrors = currentPost[ module.data.ampValidityRestField ].errors; + validationErrors = _.map( + _.filter( currentPost[ module.data.ampValidityRestField ].results, function( result ) { + return ! result.sanitized; + } ), + function( result ) { + return result.error; + } + ); blockOrder = module.getFlattenedBlockOrder( editorSelect.getBlocks() ); otherValidationErrors = []; diff --git a/includes/validation/class-amp-validation-manager.php b/includes/validation/class-amp-validation-manager.php index 9e7c49f6744..4deccb253ef 100644 --- a/includes/validation/class-amp-validation-manager.php +++ b/includes/validation/class-amp-validation-manager.php @@ -302,26 +302,22 @@ public static function get_amp_validity_rest_field( $post_data, $field_name, $re } if ( empty( $validation_status_post ) ) { - // @todo Consider process_markup() if not post type is not viewable and if post type supports editor. $validation_status_post = AMP_Invalid_URL_Post_Type::get_invalid_url_post( amp_get_permalink( $post->ID ) ); } $field = array( - 'errors' => array(), + 'results' => array(), 'review_link' => null, ); if ( $validation_status_post ) { - $field = array_merge( - $field, - array( - 'review_link' => get_edit_post_link( $validation_status_post->ID, 'raw' ), - 'errors' => wp_list_pluck( - AMP_Invalid_URL_Post_Type::get_invalid_url_validation_errors( $validation_status_post, array( 'ignore_accepted' => true ) ), - 'data' - ), - ) - ); + $field['review_link'] = get_edit_post_link( $validation_status_post->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['term']->term_group, + 'error' => $result['data'], + ); + } } return $field; @@ -1220,10 +1216,9 @@ public static function finalize_validation( DOMDocument $dom, $args = array() ) } if ( $args['append_validation_status_comment'] ) { - $errors = wp_list_pluck( self::$validation_results, 'error' ); - $encoded = wp_json_encode( $errors, 128 /* JSON_PRETTY_PRINT */ ); + $encoded = wp_json_encode( self::$validation_results, 128 /* JSON_PRETTY_PRINT */ ); $encoded = str_replace( '--', '\u002d\u002d', $encoded ); // Prevent "--" in strings from breaking out of HTML comments. - $comment = $dom->createComment( 'AMP_VALIDATION_ERRORS:' . $encoded . "\n" ); // @todo Rename to AMP_VALIDATION_RESULTS and then include sanitized. + $comment = $dom->createComment( 'AMP_VALIDATION_RESULTS:' . $encoded . "\n" ); $dom->documentElement->appendChild( $comment ); } } @@ -1302,14 +1297,15 @@ public static function validate_url( $url ) { ); } $response = wp_remote_retrieve_body( $r ); - if ( ! preg_match( '#.*?#s', $response, $matches ) ) { + if ( ! preg_match( '#.*?#s', $response, $matches ) ) { return new WP_Error( 'response_comment_absent' ); } - $validation_errors = json_decode( $matches[1], true ); - if ( ! is_array( $validation_errors ) ) { + $validation_results = json_decode( $matches[1], true ); + if ( ! is_array( $validation_results ) ) { return new WP_Error( 'malformed_json_validation_errors' ); } + $validation_errors = wp_list_pluck( $validation_results, 'error' ); return $validation_errors; } diff --git a/tests/test-class-amp-validation-utils.php b/tests/test-class-amp-validation-utils.php index a5c76312205..2bca7f70a77 100644 --- a/tests/test-class-amp-validation-utils.php +++ b/tests/test-class-amp-validation-utils.php @@ -1067,10 +1067,16 @@ public function test_validate_url() { ), $url ); + $validation_results = array(); + foreach ( $validation_errors as $error ) { + $sanitized = false; + $validation_results[] = compact( 'error', 'sanitized' ); + } + return array( 'body' => sprintf( '', - 'AMP_VALIDATION_ERRORS:' . wp_json_encode( $validation_errors ) + 'AMP_VALIDATION_RESULTS:' . wp_json_encode( $validation_results ) ), ); }; From 9d159d540c823fcec571cd458d05528887e4edbb Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 26 May 2018 13:35:17 -0700 Subject: [PATCH 41/55] Add prepare_validation_error_taxonomy_term helper method --- .../class-amp-invalid-url-post-type.php | 11 +++-------- .../class-amp-validation-error-taxonomy.php | 18 ++++++++++++++++++ .../class-amp-validation-manager.php | 6 ++---- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/includes/validation/class-amp-invalid-url-post-type.php b/includes/validation/class-amp-invalid-url-post-type.php index 32d6e3faf8b..45c40ff69a7 100644 --- a/includes/validation/class-amp-invalid-url-post-type.php +++ b/includes/validation/class-amp-invalid-url-post-type.php @@ -223,7 +223,6 @@ public static function get_invalid_url_post( $url ) { * * If there are no validation errors provided, then any existing amp_invalid_url post is deleted. * - * @todo Rename to validation results? * @param array $validation_errors Validation errors. * @param string $url URL on which the validation errors occurred. * @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. @@ -262,12 +261,8 @@ public static function store_validation_errors( $validation_errors, $url ) { $sources = $data['sources']; } - // @todo The next few lines should be moved to a mthod in AMP_Validation_Error_Taxonomy. - unset( $data['sources'] ); - ksort( $data ); - $description = wp_json_encode( $data ); - $term_slug = md5( $description ); - + $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. @@ -277,7 +272,7 @@ public static function store_validation_errors( $validation_errors, $url ) { 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( compact( 'description' ) ) ); + $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 ); } diff --git a/includes/validation/class-amp-validation-error-taxonomy.php b/includes/validation/class-amp-validation-error-taxonomy.php index 6ba3269087c..dae6f70cc31 100644 --- a/includes/validation/class-amp-validation-error-taxonomy.php +++ b/includes/validation/class-amp-validation-error-taxonomy.php @@ -162,6 +162,24 @@ public static function register() { } } + /** + * Prepare a validation error for lookup or insertion as taxonomy term. + * + * @param array $error Validation error. + * @return array Term fields. + */ + public static function prepare_validation_error_taxonomy_term( $error ) { + unset( $error['sources'] ); + ksort( $error ); + $description = wp_json_encode( $error ); + $term_slug = md5( $description ); + return array( + 'slug' => $term_slug, + 'name' => $term_slug, + 'description' => $description, + ); + } + /** * Get the count of validation error terms, optionally restricted by term group (e.g. accepted or rejected). * diff --git a/includes/validation/class-amp-validation-manager.php b/includes/validation/class-amp-validation-manager.php index 4deccb253ef..b985390ee7b 100644 --- a/includes/validation/class-amp-validation-manager.php +++ b/includes/validation/class-amp-validation-manager.php @@ -417,11 +417,9 @@ public static function add_validation_error( array $error, array $data = array() */ $error = apply_filters( 'amp_validation_error', $error, compact( 'node' ) ); - // @todo Move this into a helper function. - ksort( $error ); - $slug = md5( wp_json_encode( $error ) ); - $term = get_term_by( 'slug', $slug, AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ); + $term_data = AMP_Validation_Error_Taxonomy::prepare_validation_error_taxonomy_term( $error ); + $term = get_term_by( 'slug', $term_data['slug'], AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ); if ( ! empty( $term ) && AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPTED_STATUS === $term->term_group ) { $sanitized = true; } else { From 7869b63ac50fe25623c546283cfd1dd7a5308f7a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 26 May 2018 17:25:19 -0700 Subject: [PATCH 42/55] Prevent cached stylesheet validation errors from being reported twice when sanitization status changes --- .../sanitizers/class-amp-style-sanitizer.php | 37 +++++++++++++++++-- tests/test-amp-style-sanitizer.php | 5 ++- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 896132862dd..4881c9da2d2 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -605,7 +605,10 @@ private function process_stylesheet( $stylesheet, $options = array() ) { $parsed = get_transient( $cache_key . $cache_group ); } - // Make sure that the parsed stylesheet was cached with current sanitizations. + /* + * Make sure that the parsed stylesheet was cached with current sanitizations. + * The should_sanitize_validation_error method prevents duplicates from being reported. + */ if ( ! empty( $parsed['validation_results'] ) ) { foreach ( $parsed['validation_results'] as $validation_result ) { $sanitized = $this->should_sanitize_validation_error( $validation_result['error'] ); @@ -789,6 +792,17 @@ function( $matches ) use ( $selector, &$selectors_parsed ) { return compact( 'stylesheet', 'validation_results' ); } + /** + * Previous return values from calls to should_sanitize_validation_error(). + * + * This is used to prevent duplicates from being reported when the sanitization status + * changes for a validation error in a previously-cached stylesheet. + * + * @see AMP_Style_Sanitizer::should_sanitize_validation_error() + * @var array + */ + protected $previous_should_sanitize_validation_error_results = array(); + /** * Check whether or not sanitization should occur in response to validation error. * @@ -804,10 +818,25 @@ public function should_sanitize_validation_error( $validation_error, $data = arr if ( ! isset( $data['node'] ) ) { $data['node'] = $this->current_node; } - if ( ! isset( $error['sources'] ) ) { - $error['sources'] = $this->current_sources; + if ( ! isset( $validation_error['sources'] ) ) { + $validation_error['sources'] = $this->current_sources; } - return parent::should_sanitize_validation_error( $validation_error, $data ); + + /* + * This is used to prevent duplicates from being reported when the sanitization status + * changes for a validation error in a previously-cached stylesheet. + */ + $args = compact( 'validation_error', 'data' ); + foreach ( $this->previous_should_sanitize_validation_error_results as $result ) { + if ( $result['args'] === $args ) { + return $result['sanitized']; + } + } + + $sanitized = parent::should_sanitize_validation_error( $validation_error, $data ); + + $this->previous_should_sanitize_validation_error_results[] = compact( 'args', 'sanitized' ); + return $sanitized; } /** diff --git a/tests/test-amp-style-sanitizer.php b/tests/test-amp-style-sanitizer.php index 7726fba5a8d..dbd10190662 100644 --- a/tests/test-amp-style-sanitizer.php +++ b/tests/test-amp-style-sanitizer.php @@ -107,10 +107,11 @@ public function get_body_style_attribute_data() { ), 'illegal_unsafe_properties' => array( - '', + '', '', array( - 'button{font-weight:bold}@media screen{button{font-weight:bold}}', + 'button{font-weight:bold}', + '@media screen{button{font-weight:bold}}', ), array( 'illegal_css_property', 'illegal_css_property', 'illegal_css_property', 'illegal_css_property' ), ), From 1f315a85f372f47e5c185a3f68f31fd83268cb63 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 26 May 2018 17:25:59 -0700 Subject: [PATCH 43/55] Fix static analysis complaints in AMP_Style_Sanitizer --- composer.lock | 8 ++++---- includes/sanitizers/class-amp-style-sanitizer.php | 11 +++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/composer.lock b/composer.lock index 4579f0f7cd1..fcf465756a0 100644 --- a/composer.lock +++ b/composer.lock @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/xwp/PHP-CSS-Parser.git", - "reference": "e3204589287c28396b3db16b92ec30dab19ac2e9" + "reference": "69a4b9a002745a47120a1c02f2ece0859917694b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/xwp/PHP-CSS-Parser/zipball/e3204589287c28396b3db16b92ec30dab19ac2e9", - "reference": "e3204589287c28396b3db16b92ec30dab19ac2e9", + "url": "https://api.github.com/repos/xwp/PHP-CSS-Parser/zipball/69a4b9a002745a47120a1c02f2ece0859917694b", + "reference": "69a4b9a002745a47120a1c02f2ece0859917694b", "shasum": "" }, "require": { @@ -50,7 +50,7 @@ "support": { "source": "https://github.com/xwp/PHP-CSS-Parser/tree/master" }, - "time": "2018-04-01 07:35:36" + "time": "2018-05-27 00:18:56" } ], "packages-dev": [ diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 4881c9da2d2..d1714b22dd0 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -9,7 +9,6 @@ use \Sabberworm\CSS\CSSList\CSSList; use \Sabberworm\CSS\Property\Selector; use \Sabberworm\CSS\RuleSet\RuleSet; -use \Sabberworm\CSS\Rule\Rule; use \Sabberworm\CSS\Property\AtRule; use \Sabberworm\CSS\CSSList\KeyFrame; use \Sabberworm\CSS\RuleSet\AtRuleSet; @@ -320,6 +319,11 @@ public function sanitize() { // If 'width' attribute is present for 'col' tag, convert to proper CSS rule. foreach ( $this->dom->getElementsByTagName( 'col' ) as $col ) { + /** + * Col element. + * + * @var DOMElement $col + */ $width_attr = $col->getAttribute( 'width' ); if ( ! empty( $width_attr ) && ( false === strpos( $width_attr, '*' ) ) ) { $width_style = 'width: ' . $width_attr; @@ -1071,7 +1075,7 @@ private function process_css_declaration_block( RuleSet $ruleset, CSSList $css_l } if ( $ruleset instanceof AtRuleSet && 'font-face' === $ruleset->atRuleName() ) { - $this->process_font_face_at_rule( $ruleset, $options ); + $this->process_font_face_at_rule( $ruleset ); } $results = array_merge( @@ -1093,9 +1097,8 @@ private function process_css_declaration_block( RuleSet $ruleset, CSSList $css_l * @since 1.0 * * @param AtRuleSet $ruleset Ruleset for @font-face. - * @param array $options Options. */ - private function process_font_face_at_rule( AtRuleSet $ruleset, $options ) { + private function process_font_face_at_rule( AtRuleSet $ruleset ) { $src_properties = $ruleset->getRules( 'src' ); if ( empty( $src_properties ) ) { return; From 85a0585977281870f9d3a96b54b0487436e1d743 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 26 May 2018 19:52:52 -0700 Subject: [PATCH 44/55] Disable document.write() at DOM ready when serving dirty AMP --- includes/class-amp-theme-support.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/includes/class-amp-theme-support.php b/includes/class-amp-theme-support.php index 2723c54e6ea..ebcb66211d6 100644 --- a/includes/class-amp-theme-support.php +++ b/includes/class-amp-theme-support.php @@ -1120,6 +1120,16 @@ public static function prepare_response( $response, $args = array() ) { if ( ! AMP_Validation_Manager::should_validate_response() && AMP_Validation_Manager::has_blocking_validation_errors() ) { if ( amp_is_canonical() ) { $dom->documentElement->removeAttribute( 'amp' ); + + /* + * Make sure that document.write() is disabled to prevent dynamically-added content (such as added + * via amp-live-list) from wiping out the page by introducing any scripts that call this function. + */ + if ( $head ) { + $script = $dom->createElement( 'script' ); + $script->appendChild( $dom->createTextNode( 'document.addEventListener( "DOMContentLoaded", function() { document.write = function( text ) { throw new Error( "[AMP-WP] Prevented document.write() call with: " + text ); }; } );' ) ); + $head->appendChild( $script ); + } } else { self::redirect_canonical_amp( false ); return esc_html__( 'Redirecting to non-AMP version.', 'amp' ); From 7c0a02d060f8ec00fbd4a5d8e29394858b99765c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 26 May 2018 19:58:24 -0700 Subject: [PATCH 45/55] Remove process_markup method from validation manager Fix additional static analysis issues --- includes/class-amp-theme-support.php | 24 ++++++--- .../class-amp-validation-error-taxonomy.php | 7 +-- .../class-amp-validation-manager.php | 32 +----------- tests/test-class-amp-validation-utils.php | 49 +++++++++++++------ 4 files changed, 57 insertions(+), 55 deletions(-) diff --git a/includes/class-amp-theme-support.php b/includes/class-amp-theme-support.php index ebcb66211d6..54b8da57064 100644 --- a/includes/class-amp-theme-support.php +++ b/includes/class-amp-theme-support.php @@ -205,7 +205,13 @@ public static function is_paired_available() { return false; } - if ( is_singular() && ! post_supports_amp( get_queried_object() ) ) { + /** + * Queried object. + * + * @var WP_Post $queried_object + */ + $queried_object = get_queried_object(); + if ( is_singular() && ! post_supports_amp( $queried_object ) ) { return false; } @@ -417,7 +423,7 @@ protected static function wp_kses_amp_mustache( $text ) { * * @param string $url Comment permalink to redirect to. * @param WP_Comment $comment Posted comment. - * @return string URL. + * @return string|null URL if redirect to be done; otherwise function will exist. */ public static function filter_comment_post_redirect( $url, $comment ) { $theme_support = get_theme_support( 'amp' ); @@ -452,6 +458,7 @@ public static function filter_comment_post_redirect( $url, $comment ) { wp_send_json( array( 'message' => self::wp_kses_amp_mustache( $message ), ) ); + return null; } /** @@ -857,6 +864,13 @@ public static function print_amp_styles() { * @param DOMDocument $dom Doc. */ public static function ensure_required_markup( DOMDocument $dom ) { + /** + * Elements. + * + * @var DOMElement $meta + * @var DOMElement $script + * @var DOMElement $link + */ $head = $dom->getElementsByTagName( 'head' )->item( 0 ); if ( ! $head ) { $head = $dom->createElement( 'head' ); @@ -865,11 +879,6 @@ public static function ensure_required_markup( DOMDocument $dom ) { $meta_charset = null; $meta_viewport = null; foreach ( $head->getElementsByTagName( 'meta' ) as $meta ) { - /** - * Meta. - * - * @var DOMElement $meta - */ if ( $meta->hasAttribute( 'charset' ) && 'utf-8' === strtolower( $meta->getAttribute( 'charset' ) ) ) { // @todo Also look for meta[http-equiv="Content-Type"]? $meta_charset = $meta; } elseif ( 'viewport' === $meta->getAttribute( 'name' ) ) { @@ -1058,6 +1067,7 @@ public static function prepare_response( $response, $args = array() ) { ); // Return cache if enabled and found. + $response_cache_key = null; if ( true === $args['enable_response_caching'] ) { // Set response cache hash, the data values dictates whether a new hash key should be generated or not. $response_cache_key = md5( wp_json_encode( array( diff --git a/includes/validation/class-amp-validation-error-taxonomy.php b/includes/validation/class-amp-validation-error-taxonomy.php index dae6f70cc31..64ee9e2a2bd 100644 --- a/includes/validation/class-amp-validation-error-taxonomy.php +++ b/includes/validation/class-amp-validation-error-taxonomy.php @@ -571,11 +571,12 @@ public static function add_admin_menu_validation_error_item() { $menu_item_label .= ' ' . esc_html( number_format_i18n( $new_error_count ) ) . ''; } + $taxonomy_caps = (object) get_taxonomy( self::TAXONOMY_SLUG )->cap; // Yes, cap is an object not an array. add_submenu_page( AMP_Options_Manager::OPTION_NAME, esc_html__( 'Validation Errors', 'amp' ), $menu_item_label, - get_taxonomy( self::TAXONOMY_SLUG )->cap->manage_terms, // Yes, cap is an object not an array. + $taxonomy_caps->manage_terms, // The following esc_attr() is sadly needed due to . esc_attr( 'edit-tags.php?taxonomy=' . self::TAXONOMY_SLUG . '&post_type=' . AMP_Invalid_URL_Post_Type::POST_TYPE_SLUG ) ); @@ -858,8 +859,8 @@ public static function handle_inline_edit_request() { } $action = sanitize_key( $_GET['action'] ); // WPCS: CSRF ok. check_admin_referer( $action ); - $tax = get_taxonomy( self::TAXONOMY_SLUG ); - if ( ! current_user_can( $tax->cap->manage_terms ) ) { // Yes it is an object. + $taxonomy_caps = (object) get_taxonomy( self::TAXONOMY_SLUG )->cap; // Yes, cap is an object not an array. + if ( ! current_user_can( $taxonomy_caps->manage_terms ) ) { return; } diff --git a/includes/validation/class-amp-validation-manager.php b/includes/validation/class-amp-validation-manager.php index b985390ee7b..29a126639f2 100644 --- a/includes/validation/class-amp-validation-manager.php +++ b/includes/validation/class-amp-validation-manager.php @@ -323,31 +323,6 @@ public static function get_amp_validity_rest_field( $post_data, $field_name, $re return $field; } - /** - * Processes markup, to determine AMP validity. - * - * Passes $markup through the AMP sanitizers. - * Also passes a 'validation_error_callback' to keep track of stripped attributes and nodes. - * - * @todo Eliminate since unused. - * - * @param string $markup The markup to process. - * @return string Sanitized markup. - */ - public static function process_markup( $markup ) { - AMP_Theme_Support::register_content_embed_handlers(); - - /** 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, - 'validation_error_callback' => 'AMP_Validation_Manager::add_validation_error', - ); - - $results = AMP_Content_Sanitizer::sanitize( $markup, amp_get_content_sanitizers(), $args ); - return $results[0]; - } - /** * Whether the user has the required capability. * @@ -362,12 +337,7 @@ public static function has_cap() { /** * Add validation error. * - * @param array $error { - * Data. - * - * @type string $code Error code. - * @type DOMElement|DOMNode $node The removed node. - * } + * @param array $error Error info, especially code. * @param array $data Additional data, including the node. * * @return bool Whether the validation error should result in sanitization. diff --git a/tests/test-class-amp-validation-utils.php b/tests/test-class-amp-validation-utils.php index 2bca7f70a77..ae1e23bb38b 100644 --- a/tests/test-class-amp-validation-utils.php +++ b/tests/test-class-amp-validation-utils.php @@ -295,42 +295,63 @@ public function test_was_node_removed() { } /** - * Test process_markup. + * Processes markup, to determine AMP validity. + * + * Passes $markup through the AMP sanitizers. + * Also passes a 'validation_error_callback' to keep track of stripped attributes and nodes. * - * @covers AMP_Validation_Manager::process_markup() + * @param string $markup The markup to process. + * @return string Sanitized markup. + */ + public function process_markup( $markup ) { + AMP_Theme_Support::register_content_embed_handlers(); + + /** 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, + 'validation_error_callback' => 'AMP_Validation_Manager::add_validation_error', + ); + + $results = AMP_Content_Sanitizer::sanitize( $markup, amp_get_content_sanitizers(), $args ); + return $results[0]; + } + + /** + * Test process_markup. */ public function test_process_markup() { add_filter( 'amp_validation_error_sanitized', '__return_true' ); $this->set_capability(); - AMP_Validation_Manager::process_markup( $this->valid_amp_img ); + $this->process_markup( $this->valid_amp_img ); $this->assertEquals( array(), AMP_Validation_Manager::$validation_results ); AMP_Validation_Manager::reset_validation_results(); $video = '