array( 'name' => _x( 'AMP Validated URLs', 'post type general name', 'amp' ), 'menu_name' => __( 'Validated URLs', 'amp' ), 'singular_name' => __( 'Validated URL', 'amp' ), 'not_found' => __( 'No validated URLs found', 'amp' ), 'not_found_in_trash' => __( 'No forgotten validated URLs', 'amp' ), 'search_items' => __( 'Search validated URLs', 'amp' ), 'edit_item' => '', // Overwritten in JS, so this prevents the page header from appearing and changing. ), 'supports' => false, 'public' => false, 'show_ui' => true, 'show_in_menu' => ( self::should_show_in_menu() || AMP_Validation_Error_Taxonomy::should_show_in_menu() ) ? AMP_Options_Manager::OPTION_NAME : false, // @todo Show in rest. ) ); // Hide the add new post link. $post_type->cap->create_posts = 'do_not_allow'; if ( is_admin() ) { self::add_admin_hooks(); } } /** * Handle update to plugin. * * @param string $old_version Old version. */ public static function handle_plugin_update( $old_version ) { // Update the old post type slug from amp_validated_url to amp_validated_url. if ( '1.0-' === substr( $old_version, 0, 4 ) || version_compare( $old_version, '1.0', '<' ) ) { global $wpdb; $post_ids = get_posts( array( 'post_type' => 'amp_invalid_url', 'fields' => 'ids', 'posts_per_page' => -1, ) ); foreach ( $post_ids as $post_id ) { $wpdb->update( $wpdb->posts, array( 'post_type' => self::POST_TYPE_SLUG ), array( 'ID' => $post_id ) ); clean_post_cache( $post_id ); } } } /** * Determine whether the admin menu item should be included. * * @return bool Whether to show in menu. */ public static function should_show_in_menu() { global $pagenow; if ( AMP_Options_Manager::is_website_experience_enabled() && current_theme_supports( AMP_Theme_Support::SLUG ) ) { return true; } return ( 'edit.php' === $pagenow && ( isset( $_GET['post_type'] ) && self::POST_TYPE_SLUG === $_GET['post_type'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } /** * Add admin hooks. */ public static function add_admin_hooks() { add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_post_list_screen_scripts' ) ); if ( AMP_Options_Manager::is_website_experience_enabled() ) { add_filter( 'dashboard_glance_items', array( __CLASS__, 'filter_dashboard_glance_items' ) ); add_action( 'rightnow_end', array( __CLASS__, 'print_dashboard_glance_styles' ) ); } // Edit post screen hooks. add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_edit_post_screen_scripts' ) ); add_action( 'add_meta_boxes', array( __CLASS__, 'add_meta_boxes' ) ); add_action( 'edit_form_after_title', array( __CLASS__, 'render_single_url_list_table' ) ); add_filter( 'edit_' . AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG . '_per_page', array( __CLASS__, 'get_terms_per_page' ) ); add_action( 'admin_init', array( __CLASS__, 'add_taxonomy' ) ); add_action( 'edit_form_top', array( __CLASS__, 'print_url_as_title' ) ); // Post list screen hooks. add_filter( 'view_mode_post_types', function( $post_types ) { return array_diff( $post_types, array( AMP_Validated_URL_Post_Type::POST_TYPE_SLUG ) ); } ); add_action( 'load-edit.php', function() { if ( 'edit-' . AMP_Validated_URL_Post_Type::POST_TYPE_SLUG !== get_current_screen()->id ) { return; } add_action( 'admin_head-edit.php', function() { global $mode; $mode = 'list'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited } ); } ); add_action( 'admin_notices', array( __CLASS__, 'render_link_to_error_index_screen' ) ); add_filter( 'the_title', array( __CLASS__, 'filter_the_title_in_post_list_table' ), 10, 2 ); add_action( 'restrict_manage_posts', array( __CLASS__, 'render_post_filters' ), 10, 2 ); add_filter( 'manage_' . self::POST_TYPE_SLUG . '_posts_columns', array( __CLASS__, 'add_post_columns' ) ); add_filter( 'manage_' . self::POST_TYPE_SLUG . '_columns', array( __CLASS__, 'add_single_post_columns' ) ); add_action( 'manage_posts_custom_column', array( __CLASS__, 'output_custom_column' ), 10, 2 ); add_filter( 'bulk_actions-edit-' . self::POST_TYPE_SLUG, array( __CLASS__, 'filter_bulk_actions' ), 10, 2 ); add_filter( 'bulk_actions-' . self::POST_TYPE_SLUG, '__return_false' ); add_filter( 'handle_bulk_actions-edit-' . self::POST_TYPE_SLUG, array( __CLASS__, 'handle_bulk_action' ), 10, 3 ); add_action( 'admin_notices', array( __CLASS__, 'print_admin_notice' ) ); add_action( 'admin_action_' . self::VALIDATE_ACTION, array( __CLASS__, 'handle_validate_request' ) ); add_action( 'post_action_' . self::UPDATE_POST_TERM_STATUS_ACTION, array( __CLASS__, 'handle_validation_error_status_update' ) ); add_action( 'admin_menu', array( __CLASS__, 'add_admin_menu_new_invalid_url_count' ) ); add_filter( 'post_row_actions', array( __CLASS__, 'filter_post_row_actions' ), 10, 2 ); add_filter( sprintf( 'views_edit-%s', self::POST_TYPE_SLUG ), array( __CLASS__, 'filter_table_views' ) ); add_filter( 'bulk_post_updated_messages', array( __CLASS__, 'filter_bulk_post_updated_messages' ), 10, 2 ); add_filter( 'admin_title', array( __CLASS__, 'filter_admin_title' ) ); // Hide irrelevant "published" label in the AMP Validated URLs post list. add_filter( 'post_date_column_status', function ( $status, $post ) { if ( AMP_Validated_URL_Post_Type::POST_TYPE_SLUG === get_post_type( $post ) ) { $status = ''; } return $status; }, 10, 2 ); // Prevent query vars from persisting after redirect. add_filter( 'removable_query_args', function ( $query_vars ) { $query_vars[] = 'amp_actioned'; $query_vars[] = 'amp_taxonomy_terms_updated'; $query_vars[] = AMP_Validated_URL_Post_Type::REMAINING_ERRORS; $query_vars[] = 'amp_urls_tested'; $query_vars[] = 'amp_validate_error'; return $query_vars; } ); } /** * Enqueue style. */ public static function enqueue_post_list_screen_scripts() { $screen = get_current_screen(); if ( 'edit-' . self::POST_TYPE_SLUG === $screen->id && self::POST_TYPE_SLUG === $screen->post_type ) { $script_deps_path = AMP__DIR__ . '/assets/js/amp-validated-urls-index.deps.json'; $script_dependencies = file_exists( $script_deps_path ) ? json_decode( file_get_contents( $script_deps_path ), false ) // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents : array(); wp_enqueue_script( 'amp-validated-urls-index', amp_get_asset_url( 'js/amp-validated-urls-index.js' ), $script_dependencies, AMP__VERSION, true ); } // Enqueue this on both the 'AMP Validated URLs' page and the single URL page. if ( 'edit-' . self::POST_TYPE_SLUG === $screen->id || self::POST_TYPE_SLUG === $screen->id ) { wp_enqueue_style( 'amp-admin-tables', amp_get_asset_url( 'css/admin-tables.css' ), false, AMP__VERSION ); } if ( 'edit-' . self::POST_TYPE_SLUG !== $screen->id ) { return; } wp_register_style( 'amp-validation-tooltips', amp_get_asset_url( 'css/amp-validation-tooltips.css' ), array( 'wp-pointer' ), AMP__VERSION ); $script_deps_path = AMP__DIR__ . '/assets/js/amp-validation-tooltips.deps.json'; $script_dependencies = file_exists( $script_deps_path ) ? json_decode( file_get_contents( $script_deps_path ), false ) // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents : array(); wp_register_script( 'amp-validation-tooltips', amp_get_asset_url( 'js/amp-validation-tooltips.js' ), array_merge( $script_dependencies, array( 'wp-pointer' ) ), AMP__VERSION, true ); wp_enqueue_style( 'amp-validation-error-taxonomy', amp_get_asset_url( 'css/amp-validation-error-taxonomy.css' ), array( 'common', 'amp-validation-tooltips' ), AMP__VERSION ); wp_enqueue_script( 'amp-validation-detail-toggle', amp_get_asset_url( 'js/amp-validation-detail-toggle.js' ), array( 'wp-dom-ready', 'wp-i18n', 'amp-validation-tooltips' ), AMP__VERSION, true ); } /** * On the 'AMP Validated URLs' screen, renders a link to the 'Error Index' page. * * @see AMP_Validation_Error_Taxonomy::render_link_to_invalid_urls_screen() */ public static function render_link_to_error_index_screen() { if ( ! ( get_current_screen() && 'edit' === get_current_screen()->base && self::POST_TYPE_SLUG === get_current_screen()->post_type ) ) { return; } $taxonomy_object = get_taxonomy( AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ); if ( ! current_user_can( $taxonomy_object->cap->manage_terms ) ) { return; } $id = 'link-errors-index'; printf( '', esc_url( get_admin_url( null, 'edit-tags.php?taxonomy=' . AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG . '&post_type=' . self::POST_TYPE_SLUG ) ), esc_attr( $id ), esc_html__( 'View Error Index', 'amp' ) ); ?> self::POST_TYPE_SLUG, AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_STATUS_QUERY_VAR => array( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_REJECTED_STATUS, AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_ACCEPTED_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 validated URL post. * * @param string|int|WP_Post $url Either the URL string or a post (ID or WP_Post) of amp_validated_url type. * @param array $args { * Args. * * @type bool $ignore_accepted Exclude validation errors that are accepted. Default false. * } * @return array List of errors, with keys for term, data, status, and (sanitization) forced. */ public static function get_invalid_url_validation_errors( $url, $args = array() ) { $args = array_merge( array( 'ignore_accepted' => false, ), $args ); // Look up post by URL or ensure the amp_validated_url object. if ( is_string( $url ) ) { $post = self::get_invalid_url_post( $url ); } else { $post = get_post( $url ); } if ( ! $post || self::POST_TYPE_SLUG !== $post->post_type ) { return array(); } // Skip when parse error. $stored_validation_errors = json_decode( $post->post_content, true ); if ( ! is_array( $stored_validation_errors ) ) { return array(); } $errors = array(); foreach ( $stored_validation_errors as $stored_validation_error ) { if ( ! isset( $stored_validation_error['term_slug'] ) ) { continue; } $term = AMP_Validation_Error_Taxonomy::get_term( $stored_validation_error['term_slug'] ); if ( ! $term ) { continue; } $sanitization = AMP_Validation_Error_Taxonomy::get_validation_error_sanitization( $stored_validation_error['data'] ); if ( $args['ignore_accepted'] && ( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_ACCEPTED_STATUS === $sanitization['status'] || AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACK_ACCEPTED_STATUS === $sanitization['status'] ) ) { continue; } $errors[] = array_merge( array( 'term' => $term, 'data' => $stored_validation_error['data'], ), $sanitization ); } return $errors; } /** * Display summary of the validation error counts for a given post. * * @param int|WP_Post $post Post of amp_validated_url type. * @param array $args { * Arguments. * * @type bool $display_enabled_status Whether to display the status of whether AMP is enabled on the URL. * } */ public static function display_invalid_url_validation_error_counts_summary( $post, $args = array() ) { $args = array_merge( array( 'display_enabled_status' => false, ), $args ); $validation_errors = self::get_invalid_url_validation_errors( $post ); $counts = self::count_invalid_url_validation_errors( $validation_errors ); $result = array(); if ( $counts['new_rejected'] ) { $result[] = sprintf( /* translators: 1: status. 2: count. */ '%1$s: %2$s', esc_html__( 'New Rejected', 'amp' ), number_format_i18n( $counts['new_rejected'] ) ); } if ( $counts['new_accepted'] ) { $result[] = sprintf( /* translators: 1: status. 2: count. */ '%1$s: %2$s', esc_html__( 'New Accepted', 'amp' ), number_format_i18n( $counts['new_accepted'] ) ); } if ( $counts['ack_accepted'] ) { $result[] = sprintf( /* translators: 1. Title, 2. %s is count */ '%1$s: %2$s', esc_html__( 'Accepted', 'amp' ), number_format_i18n( $counts['ack_accepted'] ) ); } if ( $counts['ack_rejected'] ) { $result[] = sprintf( /* translators: %s is count */ '%1$s: %2$s', esc_html__( 'Rejected', 'amp' ), number_format_i18n( $counts['ack_rejected'] ) ); } if ( $args['display_enabled_status'] ) { $is_amp_enabled = self::is_amp_enabled_on_post( $post ); $class = $is_amp_enabled ? 'sanitized' : 'new'; ?> true, 'include_trashed' => false, ); $options = wp_parse_args( $options, $default ); if ( $options['normalize'] ) { $url = self::normalize_url_for_storage( $url ); } $slug = md5( $url ); $post = get_page_by_path( $slug, OBJECT, self::POST_TYPE_SLUG ); if ( $post ) { return $post; } if ( $options['include_trashed'] ) { $post = get_page_by_path( $slug . '__trashed', OBJECT, self::POST_TYPE_SLUG ); if ( $post ) { return $post; } } return null; } /** * Get the URL from a given amp_validated_url post. * * The URL will be returned with the amp query var added to it if the site is not canonical. The post_title * is always stored using the canonical AMP-less URL. * * @param int|WP_post $post Post. * @return string|null The URL stored for the post or null if post does not exist or it is not the right type. */ public static function get_url_from_post( $post ) { $post = get_post( $post ); if ( ! $post || self::POST_TYPE_SLUG !== $post->post_type ) { return null; } $url = $post->post_title; // Add AMP query var if in transitional mode. if ( ! amp_is_canonical() ) { $url = add_query_arg( amp_get_slug(), '', $url ); } // Set URL scheme based on whether HTTPS is current. $url = set_url_scheme( $url, ( 'http' === wp_parse_url( home_url(), PHP_URL_SCHEME ) ) ? 'http' : 'https' ); return $url; } /** * Normalize a URL for storage. * * This ensures that query vars like utm_* and the like will not cause duplicates. * The AMP query param is removed to facilitate switching between standard and transitional. * The URL scheme is also normalized to HTTPS to help with transition from HTTP to HTTPS. * * @param string $url URL. * @return string Normalized URL. * @global WP $wp */ protected static function normalize_url_for_storage( $url ) { global $wp; // Only ever store the canonical version. $url = amp_remove_endpoint( $url ); // Remove fragment identifier in the rare case it could be provided. It is irrelevant for validation. $url = strtok( $url, '#' ); // Normalize query args, removing all that are not recognized or which are removable. $url_parts = explode( '?', $url, 2 ); if ( 2 === count( $url_parts ) ) { parse_str( $url_parts[1], $args ); foreach ( wp_removable_query_args() as $removable_query_arg ) { unset( $args[ $removable_query_arg ] ); } $args = wp_array_slice_assoc( $args, $wp->public_query_vars ); $url = $url_parts[0]; if ( ! empty( $args ) ) { $url = $url_parts[0] . '?' . build_query( $args ); } } // Normalize the scheme as HTTPS. $url = set_url_scheme( $url, 'https' ); return $url; } /** * Stores the validation errors. * * If there are no validation errors provided, then any existing amp_validated_url post is deleted. * * @param array $validation_errors Validation errors. * @param string $url URL on which the validation errors occurred. Will be normalized to non-AMP version. * @param array $args { * Args. * * @type int|WP_Post $invalid_url_post Post to update. Optional. If empty, then post is looked up by URL. * @type array $queried_object Queried object, including keys for type and id. May be empty. * } * @return int|WP_Error $post_id The post ID of the custom post type used, or WP_Error on failure. * @global WP $wp */ public static function store_validation_errors( $validation_errors, $url, $args = array() ) { $url = self::normalize_url_for_storage( $url ); $slug = md5( $url ); $post = null; if ( ! empty( $args['invalid_url_post'] ) ) { $post = get_post( $args['invalid_url_post'] ); } if ( ! $post ) { $post = self::get_invalid_url_post( $url, array( 'include_trashed' => true, 'normalize' => false, // Since already normalized. ) ); } $is_story = ( isset( $args['queried_object'], $args['queried_object']['type'], $args['queried_object']['id'] ) && 'post' === $args['queried_object']['type'] && AMP_Story_Post_Type::POST_TYPE_SLUG === get_post_type( $args['queried_object']['id'] ) ); /* * 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(); // Prevent Kses from corrupting JSON in description. $pre_term_description_filters = array( 'wp_filter_kses' => has_filter( 'pre_term_description', 'wp_filter_kses' ), 'wp_targeted_link_rel' => has_filter( 'pre_term_description', 'wp_targeted_link_rel' ), ); foreach ( $pre_term_description_filters as $callback => $priority ) { if ( false !== $priority ) { remove_filter( 'pre_term_description', $callback, $priority ); } } $terms = array(); foreach ( $validation_errors as $data ) { $term_data = AMP_Validation_Error_Taxonomy::prepare_validation_error_taxonomy_term( $data ); $term_slug = $term_data['slug']; if ( ! isset( $terms[ $term_slug ] ) ) { // Not using WP_Term_Query since more likely individual terms are cached and wp_insert_term() will itself look at this cache anyway. $term = AMP_Validation_Error_Taxonomy::get_term( $term_slug ); if ( ! ( $term instanceof WP_Term ) ) { /* * The default term_group is 0 so that is AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_REJECTED_STATUS. * If sanitization auto-acceptance is enabled, then the term_group will be updated below. */ $r = wp_insert_term( $term_slug, AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG, wp_slash( $term_data ) ); if ( is_wp_error( $r ) ) { continue; } $term_id = $r['term_id']; update_term_meta( $term_id, 'created_date_gmt', current_time( 'mysql', true ) ); /* * When sanitization is forced by filter, make sure the term is created with the filtered status. * For some reason, the wp_insert_term() function doesn't work with the term_group being passed in. */ $sanitization = AMP_Validation_Error_Taxonomy::get_validation_error_sanitization( $data ); if ( 'with_filter' === $sanitization['forced'] ) { $term_data['term_group'] = $sanitization['status']; wp_update_term( $term_id, AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG, array( 'term_group' => $sanitization['status'], ) ); } elseif ( AMP_Validation_Manager::is_sanitization_auto_accepted() || $is_story ) { $term_data['term_group'] = AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_ACCEPTED_STATUS; wp_update_term( $term_id, AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG, array( 'term_group' => $term_data['term_group'], ) ); } $term = get_term( $term_id ); } $terms[ $term_slug ] = $term; } $stored_validation_errors[] = compact( 'term_slug', 'data' ); } // Finish preventing Kses from corrupting JSON in description. foreach ( $pre_term_description_filters as $callback => $priority ) { if ( false !== $priority ) { add_filter( 'pre_term_description', $callback, $priority ); } } $post_content = wp_json_encode( $stored_validation_errors ); $placeholder = 'amp_validated_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'], $post_data['post_type'] ) && $placeholder === $post_data['post_content'] && AMP_Validated_URL_Post_Type::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' => $slug, 'post_content' => $placeholder, // Content is provided via wp_insert_post_data filter above to guard against Kses-corruption. 'post_status' => 'publish', ) ), 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 ); update_post_meta( $post_id, '_amp_validated_environment', self::get_validated_environment() ); if ( isset( $args['queried_object'] ) ) { update_post_meta( $post_id, '_amp_queried_object', $args['queried_object'] ); } return $post_id; } /** * Get the environment properties which will likely effect whether validation results are stale. * * @return array Environment. */ public static function get_validated_environment() { return array( 'theme' => get_stylesheet(), 'plugins' => get_option( 'active_plugins', array() ), ); } /** * Get the differences between the current themes, plugins, and relevant options when amp_validated_url post was last updated and now. * * @param int|WP_Post $post Post of amp_validated_url type. * @return array { * Staleness of the validation results. An empty array if the results are fresh. * * @type string $theme The theme that was active but is no longer. Absent if theme is the same. * @type array $plugins Plugins that used to be active but are no longer, or which are active now but weren't. Absent if the plugins were the same. * @type array $options Options that are now different. Absent if the options were the same. * } */ public static function get_post_staleness( $post ) { $post = get_post( $post ); if ( empty( $post ) || self::POST_TYPE_SLUG !== $post->post_type ) { return array(); } $old_validated_environment = get_post_meta( $post->ID, '_amp_validated_environment', true ); $new_validated_environment = self::get_validated_environment(); $staleness = array(); if ( isset( $old_validated_environment['theme'] ) && $new_validated_environment['theme'] !== $old_validated_environment['theme'] ) { $staleness['theme'] = $old_validated_environment['theme']; } if ( isset( $old_validated_environment['plugins'] ) ) { $new_active_plugins = array_diff( $new_validated_environment['plugins'], $old_validated_environment['plugins'] ); if ( ! empty( $new_active_plugins ) ) { $staleness['plugins']['new'] = array_values( $new_active_plugins ); } $old_active_plugins = array_diff( $old_validated_environment['plugins'], $new_validated_environment['plugins'] ); if ( ! empty( $old_active_plugins ) ) { $staleness['plugins']['old'] = array_values( $old_active_plugins ); } } return $staleness; } /** * 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( AMP_Validation_Error_Taxonomy::ERROR_STATUS => sprintf( '%s', esc_html__( 'Status', 'amp' ), esc_attr( sprintf( '

%s

%s

', __( 'Status', 'amp' ), __( 'An accepted validation error is one that will not block a URL from being served as AMP; the validation error will be sanitized, normally resulting in the offending markup being stripped from the response to ensure AMP validity.', 'amp' ) ) ) ), AMP_Validation_Error_Taxonomy::FOUND_ELEMENTS_AND_ATTRIBUTES => esc_html__( 'Invalid', 'amp' ), AMP_Validation_Error_Taxonomy::SOURCES_INVALID_OUTPUT => esc_html__( 'Sources', 'amp' ), ) ); if ( isset( $columns['title'] ) ) { $columns['title'] = esc_html__( 'URL', 'amp' ); } // Move date to end. if ( isset( $columns['date'] ) ) { unset( $columns['date'] ); $columns['date'] = esc_html__( 'Last Checked', 'amp' ); } if ( ! empty( $_GET[ \AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended unset( $columns['error_status'], $columns[ AMP_Validation_Error_Taxonomy::REMOVED_ELEMENTS ], $columns[ AMP_Validation_Error_Taxonomy::REMOVED_ATTRIBUTES ] ); $columns[ AMP_Validation_Error_Taxonomy::SOURCES_INVALID_OUTPUT ] = esc_html__( 'Sources', 'amp' ); $columns['date'] = esc_html__( 'Last Checked', 'amp' ); $columns['title'] = esc_html__( 'URL', 'amp' ); } return $columns; } /** * Adds post columns to the /wp-admin/post.php page for amp_validated_url. * * @return array The filtered post columns. */ public static function add_single_post_columns() { return array( 'cb' => '', 'error' => __( 'Error', 'amp' ), 'status' => sprintf( '%s', esc_html__( 'Status', 'amp' ), esc_attr( sprintf( '

%s

%s

', esc_html__( 'Status', 'amp' ), esc_html__( 'An accepted validation error is one that will not block a URL from being served as AMP; the validation error will be sanitized, normally resulting in the offending markup being stripped from the response to ensure AMP validity.', 'amp' ) ) ) ), 'details' => sprintf( '%s', esc_html__( 'Details', 'amp' ), esc_attr( sprintf( '

%s

%s

', esc_html__( 'Details', 'amp' ), esc_html__( 'The parent element of where the error occurred.', 'amp' ) ) ) ), 'sources_with_invalid_output' => __( 'Sources', 'amp' ), 'error_type' => __( 'Type', 'amp' ), ); } /** * Outputs custom columns in the /wp-admin UI for the AMP validation errors. * * @param string $column_name The name of the column. * @param int $post_id The ID of the post for the column. * @return void */ public static function output_custom_column( $column_name, $post_id ) { $post = get_post( $post_id ); if ( self::POST_TYPE_SLUG !== $post->post_type ) { return; } $validation_errors = self::get_invalid_url_validation_errors( $post_id ); $error_summary = AMP_Validation_Error_Taxonomy::summarize_validation_errors( wp_list_pluck( $validation_errors, 'data' ) ); switch ( $column_name ) { case 'error_status': $staleness = self::get_post_staleness( $post_id ); if ( ! empty( $staleness ) ) { echo '' . esc_html__( 'Stale results', 'amp' ) . '
'; } self::display_invalid_url_validation_error_counts_summary( $post_id ); break; case AMP_Validation_Error_Taxonomy::FOUND_ELEMENTS_AND_ATTRIBUTES: $items = array(); if ( ! empty( $error_summary[ AMP_Validation_Error_Taxonomy::REMOVED_ELEMENTS ] ) ) { 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 ); } } } if ( ! empty( $error_summary[ AMP_Validation_Error_Taxonomy::REMOVED_ATTRIBUTES ] ) ) { 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 ); } } } if ( ! empty( $items ) ) { $imploded_items = implode( ',
', $items ); echo sprintf( '
%s
', $imploded_items ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } else { esc_html_e( '--', 'amp' ); } break; case AMP_Validation_Error_Taxonomy::SOURCES_INVALID_OUTPUT: self::render_sources_column( $error_summary, $post_id ); break; } } /** * Renders the sources column on the the single error URL page and the 'AMP Validated URLs' page. * * @param array $error_summary The summary of errors. * @param int $post_id The ID of the amp_validated_url post. */ public static function render_sources_column( $error_summary, $post_id ) { if ( ! isset( $error_summary[ AMP_Validation_Error_Taxonomy::SOURCES_INVALID_OUTPUT ] ) ) { return; } // Show nothing if there are no valudation errors. if ( 0 === count( array_filter( $error_summary ) ) ) { esc_html_e( '--', 'amp' ); return; } $active_theme = null; $validated_environment = get_post_meta( $post_id, '_amp_validated_environment', true ); if ( isset( $validated_environment['theme'] ) ) { $active_theme = $validated_environment['theme']; } $sources = $error_summary[ AMP_Validation_Error_Taxonomy::SOURCES_INVALID_OUTPUT ]; $output = array(); $plugins = get_plugins(); foreach ( wp_array_slice_assoc( $sources, array( 'plugin', 'mu-plugin' ) ) as $type => $slugs ) { $plugin_names = array(); $plugin_slugs = array_unique( $slugs ); foreach ( $plugin_slugs as $plugin_slug ) { if ( 'mu-plugin' === $type ) { $plugin_names[] = $plugin_slug; } else { $name = $plugin_slug; foreach ( $plugins as $plugin_file => $plugin_data ) { if ( strtok( $plugin_file, '/' ) === $plugin_slug ) { $name = $plugin_data['Name']; break; } } $plugin_names[] = $name; } } $count = count( $plugin_names ); if ( 1 === $count ) { $output[] = sprintf( '%s', esc_html( $plugin_names[0] ) ); } else { $output[] = '
'; $output[] = sprintf( '%s (%d)', 'mu-plugin' === $type ? esc_html__( 'Must-Use Plugins', 'amp' ) : esc_html__( 'Plugins', 'amp' ), $count ); $output[] = '
'; $output[] = implode( '
', array_unique( $plugin_names ) ); $output[] = '
'; $output[] = '
'; } } if ( isset( $sources['theme'] ) && empty( $sources['embed'] ) ) { $output[] = '
'; $output[] = ''; $themes = array_unique( $sources['theme'] ); foreach ( $themes as $theme_slug ) { $theme_obj = wp_get_theme( $theme_slug ); if ( ! $theme_obj->errors() ) { $theme_name = $theme_obj->get( 'Name' ); } else { $theme_name = $theme_slug; } $output[] = sprintf( '%s', esc_html( $theme_name ) ); } $output[] = '
'; } if ( isset( $sources['core'] ) ) { $core_sources = array_unique( $sources['core'] ); $count = count( $core_sources ); if ( 1 === $count ) { $output[] = sprintf( '%s', esc_html( $core_sources[0] ) ); } else { $output[] = '
'; $output[] = sprintf( '%s (%d)', esc_html__( 'Other', 'amp' ), $count ); $output[] = '
'; $output[] = implode( '
', array_unique( $sources['core'] ) ); $output[] = '
'; $output[] = '
'; } } if ( empty( $output ) && ! empty( $sources['embed'] ) ) { $output[] = sprintf( '%s', esc_html( 'Embed' ) ); } if ( empty( $output ) && ! empty( $sources['hook'] ) ) { $output[] = sprintf( '%s', esc_html( $sources['hook'] ) ); } if ( empty( $sources ) && $active_theme ) { $theme_obj = wp_get_theme( $active_theme ); if ( ! $theme_obj->errors() ) { $theme_name = $theme_obj->get( 'Name' ); } else { $theme_name = $active_theme; } $output[] = '
'; $output[] = ''; /* translators: %s is the guessed theme as the source for the error */ $output[] = esc_html( sprintf( __( '%s (?)', 'amp' ), $theme_name ) ); $output[] = '
'; } echo implode( '', $output ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Adds a 'Recheck' bulk action to the edit.php page and modifies the 'Move to Trash' text. * * Ensure only delete action is present, not trash. * * @param array $actions The bulk actions in the edit.php page. * @return array $actions The filtered bulk actions. */ public static function filter_bulk_actions( $actions ) { $has_delete = ( isset( $actions['trash'] ) || isset( $actions['delete'] ) ); unset( $actions['trash'], $actions['delete'] ); if ( $has_delete ) { $actions['delete'] = esc_html__( 'Forget', 'amp' ); } unset( $actions['edit'] ); $actions[ self::BULK_VALIDATE_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::BULK_VALIDATE_ACTION !== $action ) { return $redirect; } $remaining_invalid_urls = array(); $errors = array(); foreach ( $items as $item ) { $post = get_post( $item ); if ( empty( $post ) || ! current_user_can( 'edit_post', $post->ID ) ) { continue; } $url = self::get_url_from_post( $post ); if ( empty( $url ) ) { continue; } $validity = AMP_Validation_Manager::validate_url( $url ); if ( is_wp_error( $validity ) ) { $errors[] = $validity->get_error_code(); continue; } $validation_errors = wp_list_pluck( $validity['results'], 'error' ); self::store_validation_errors( $validation_errors, $validity['url'], wp_array_slice_assoc( $validity, array( 'queried_object' ) ) ); $unaccepted_error_count = count( array_filter( $validation_errors, function( $error ) { return ! AMP_Validation_Error_Taxonomy::is_validation_error_sanitized( $error ); } ) ); if ( $unaccepted_error_count > 0 ) { $remaining_invalid_urls[] = $validity['url']; } } // Get the URLs that still have errors after rechecking. $args = array( self::URLS_TESTED => count( $items ), ); if ( ! empty( $errors ) ) { $args['amp_validate_error'] = $errors; } else { $args[ self::REMAINING_ERRORS ] = count( $remaining_invalid_urls ); } $redirect = remove_query_arg( wp_removable_query_args(), $redirect ); return add_query_arg( $args, $redirect ); } /** * Outputs an admin notice after rechecking URL(s) on the custom post page. * * @return void */ public static function print_admin_notice() { if ( ! get_current_screen() || self::POST_TYPE_SLUG !== get_current_screen()->post_type ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } if ( isset( $_GET['amp_validate_error'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $error_codes = array_unique( array_map( 'sanitize_key', (array) $_GET['amp_validate_error'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended foreach ( $error_codes as $error_code ) { printf( '

%s

', esc_html( AMP_Validation_Manager::get_validate_url_error_message( $error_code ) ), esc_html__( 'Dismiss this notice.', 'amp' ) ); } } if ( isset( $_GET[ self::REMAINING_ERRORS ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $count_urls_tested = isset( $_GET[ self::URLS_TESTED ] ) ? intval( $_GET[ self::URLS_TESTED ] ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $errors_remain = ! empty( $_GET[ self::REMAINING_ERRORS ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $errors_remain ) { $message = _n( 'The rechecked URL still has unaccepted validation errors.', 'The rechecked URLs still have unaccepted validation errors.', $count_urls_tested, 'amp' ); $class = 'notice-warning'; } else { $message = _n( 'The rechecked URL is free of unaccepted validation errors.', 'The rechecked URLs are free of unaccepted validation errors.', $count_urls_tested, 'amp' ); $class = 'updated'; } printf( '

%s

', esc_attr( $class ), esc_html( $message ), esc_html__( 'Dismiss this notice.', 'amp' ) ); } $count = isset( $_GET['amp_taxonomy_terms_updated'] ) ? intval( $_GET['amp_taxonomy_terms_updated'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $count > 0 ) { $class = 'updated'; printf( '

%s

', esc_attr( $class ), esc_html( sprintf( /* translators: %s is count of validation errors updated */ _n( 'Updated %s validation error.', 'Updated %s validation errors.', $count, 'amp' ), number_format_i18n( $count ) ) ), esc_html__( 'Dismiss this notice.', 'amp' ) ); } if ( 'post' !== get_current_screen()->base ) { // Display admin notice according to the AMP mode. if ( amp_is_canonical() ) { $template_mode = AMP_Theme_Support::STANDARD_MODE_SLUG; } elseif ( current_theme_supports( AMP_Theme_Support::SLUG ) ) { $template_mode = AMP_Theme_Support::TRANSITIONAL_MODE_SLUG; } else { $template_mode = 'reader'; } $auto_sanitization = AMP_Options_Manager::get_option( 'auto_accept_sanitization' ); if ( AMP_Theme_Support::STANDARD_MODE_SLUG === $template_mode ) { $message = __( 'The site is using standard AMP mode, the validation errors found are already automatically handled.', 'amp' ); } elseif ( AMP_Theme_Support::TRANSITIONAL_MODE_SLUG === $template_mode && $auto_sanitization ) { $message = __( 'The site is using transitional AMP mode with auto-sanitization turned on, the validation errors found are already automatically handled.', 'amp' ); } elseif ( AMP_Theme_Support::TRANSITIONAL_MODE_SLUG === $template_mode ) { $message = sprintf( /* translators: %s is a link to the AMP settings screen */ __( 'The site is using transitional AMP mode without auto-sanitization, the validation errors found require action and influence which pages are shown in AMP. For automatically handling the errors turn on auto-sanitization from Validation Handling settings.', 'amp' ), esc_url( admin_url( 'admin.php?page=' . AMP_Options_Manager::OPTION_NAME ) ) ); } else { $message = __( 'The site is using AMP reader mode, your theme templates are not used and the errors below are irrelevant.', 'amp' ); } $class = 'info'; printf( /* translators: 1. Notice classname; 2. Message text; 3. Screenreader text; */ '

%s

', esc_attr( $class ), wp_kses_post( $message ) ); } /** * Adds notices to the single error page. * 1. Notice with detailed error information in an expanding box. * 2. Notice with accept and reject buttons. */ if ( ! empty( $_GET[ \AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ] ) && isset( $_GET['post_type'] ) && self::POST_TYPE_SLUG === $_GET['post_type'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $error_id = sanitize_key( wp_unslash( $_GET[ \AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $error = AMP_Validation_Error_Taxonomy::get_term( $error_id ); if ( ! $error ) { return; } // @todo Update this to use the method which will be developed in PR #1429 AMP_Validation_Error_Taxonomy::get_term_error() . $description = json_decode( $error->description, true ); $sanitization = \AMP_Validation_Error_Taxonomy::get_validation_error_sanitization( $description ); $status_text = \AMP_Validation_Error_Taxonomy::get_status_text_with_icon( $sanitization ); $error_code = isset( $description['code'] ) ? $description['code'] : 'error'; $error_title = \AMP_Validation_Error_Taxonomy::get_error_title_from_code( $error_code ); $validation_error = json_decode( $error->description, true ); $accept_all_url = wp_nonce_url( add_query_arg( array( 'action' => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPT_ACTION, 'term_id' => $error->term_id, ) ), AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACCEPT_ACTION ); $reject_all_url = wp_nonce_url( add_query_arg( array( 'action' => AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECT_ACTION, 'term_id' => $error->term_id, ) ), AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_REJECT_ACTION ); if ( ! $sanitization['forced'] ) { echo '
'; if ( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_ACCEPTED_STATUS === $sanitization['term_status'] || AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACK_ACCEPTED_STATUS === $sanitization['term_status'] ) { if ( amp_is_canonical() ) { $info = __( 'Rejecting an error means that any URL on which it occurs will not be served as AMP.', 'amp' ); } else { $info = __( 'Rejecting an error means that any URL on which it occurs will redirect to the non-AMP version.', 'amp' ); } printf( '

%s

%s', esc_html__( 'Reject this validation error for all instances.', 'amp' ) . ' ' . esc_html( $info ), esc_url( $reject_all_url ), esc_html__( 'Reject', 'amp' ) ); } elseif ( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_REJECTED_STATUS === $sanitization['term_status'] || AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACK_REJECTED_STATUS === $sanitization['term_status'] ) { if ( amp_is_canonical() ) { $info = __( 'Accepting all validation errors which occur on a URL will allow it to be served as AMP.', 'amp' ); } else { $info = __( 'Accepting all validation errors which occur on a URL will allow it to be served as AMP.', 'amp' ); } printf( '

%s

%s', esc_html__( 'Accept this error for all instances.', 'amp' ) . ' ' . esc_html( $info ), esc_url( $accept_all_url ), esc_html__( 'Accept', 'amp' ) ); } else { if ( amp_is_canonical() ) { $info = __( 'Rejecting an error means that any URL on which it occurs will not be served as AMP. If all errors occurring on a URL are accepted, then it will be served as AMP.', 'amp' ); } else { $info = __( 'Rejecting an error means that any URL on which it occurs will redirect to the non-AMP version. If all errors occurring on a URL are accepted, then it will not redirect.', 'amp' ); } printf( '

%s

%s%s', esc_html__( 'Accept or Reject this error for all instances.', 'amp' ) . ' ' . esc_html( $info ), esc_url( $reject_all_url ), esc_html__( 'Reject', 'amp' ), esc_url( $accept_all_url ), esc_html__( 'Accept', 'amp' ) ); } echo '
'; } ?>
%s%s', esc_html( $error_title ), esc_html( $description['node_name'] ), wp_kses_post( $status_text ) ); ?> post_type ) { throw new Exception( 'invalid_post' ); } if ( ! current_user_can( 'edit_post', $post->ID ) ) { throw new Exception( 'unauthorized' ); } $url = self::get_url_from_post( $post ); } elseif ( isset( $_GET['url'] ) ) { $url = wp_validate_redirect( esc_url_raw( wp_unslash( $_GET['url'] ) ), null ); if ( ! $url ) { throw new Exception( 'illegal_url' ); } // Don't let non-admins create new amp_validated_url posts. if ( ! current_user_can( 'manage_options' ) ) { throw new Exception( 'unauthorized' ); } } if ( ! $url ) { throw new Exception( 'missing_url' ); } $validity = AMP_Validation_Manager::validate_url( $url ); if ( is_wp_error( $validity ) ) { throw new Exception( esc_html( $validity->get_error_code() ) ); } $errors = wp_list_pluck( $validity['results'], 'error' ); $stored = self::store_validation_errors( $errors, $validity['url'], array_merge( array( 'invalid_url_post' => $post, ), wp_array_slice_assoc( $validity, array( 'queried_object' ) ) ) ); if ( is_wp_error( $stored ) ) { throw new Exception( esc_html( $stored->get_error_code() ) ); } $redirect = get_edit_post_link( $stored, 'raw' ); $error_count = count( array_filter( $errors, function ( $error ) { return ! AMP_Validation_Error_Taxonomy::is_validation_error_sanitized( $error ); } ) ); $args[ self::URLS_TESTED ] = '1'; $args[ self::REMAINING_ERRORS ] = $error_count; } catch ( Exception $e ) { $args['amp_validate_error'] = $e->getMessage(); $args[ self::URLS_TESTED ] = '0'; if ( $post && self::POST_TYPE_SLUG === $post->post_type ) { $redirect = get_edit_post_link( $post->ID, 'raw' ); } else { $redirect = admin_url( add_query_arg( array( 'post_type' => self::POST_TYPE_SLUG ), 'edit.php' ) ); } } wp_safe_redirect( add_query_arg( $args, $redirect ) ); exit(); } /** * Re-check validated URL post for whether it has blocking validation errors. * * @param int|WP_Post $post Post. * @return array|WP_Error List of blocking validation results, or a WP_Error in the case of failure. */ public static function recheck_post( $post ) { if ( ! $post ) { return new WP_Error( 'missing_post' ); } $post = get_post( $post ); if ( ! $post ) { return new WP_Error( 'missing_post' ); } $url = self::get_url_from_post( $post ); if ( ! $url ) { return new WP_Error( 'missing_url' ); } $validity = AMP_Validation_Manager::validate_url( $url ); if ( is_wp_error( $validity ) ) { return $validity; } $validation_errors = wp_list_pluck( $validity['results'], 'error' ); $validation_results = array(); self::store_validation_errors( $validation_errors, $validity['url'], array_merge( array( 'invalid_url_post' => $post, ), wp_array_slice_assoc( $validity, array( 'queried_object' ) ) ) ); foreach ( $validation_errors as $error ) { $sanitized = AMP_Validation_Error_Taxonomy::is_validation_error_sanitized( $error ); // @todo Consider re-using $validity['results'][x]['sanitized'], unless auto-sanitize is causing problem. $validation_results[] = compact( 'error', 'sanitized' ); } return $validation_results; } /** * Handle validation error status update. * * @see AMP_Validation_Error_Taxonomy::handle_validation_error_update() * @todo This is duplicated with logic in AMP_Validation_Error_Taxonomy. All of the term updating needs to be refactored to make use of the REST API. */ public static function handle_validation_error_status_update() { check_admin_referer( self::UPDATE_POST_TERM_STATUS_ACTION, self::UPDATE_POST_TERM_STATUS_ACTION . '_nonce' ); if ( empty( $_POST[ AMP_Validation_Manager::VALIDATION_ERROR_TERM_STATUS_QUERY_VAR ] ) || ! is_array( $_POST[ AMP_Validation_Manager::VALIDATION_ERROR_TERM_STATUS_QUERY_VAR ] ) ) { return; } $post = get_post(); if ( ! $post || self::POST_TYPE_SLUG !== $post->post_type ) { return; } if ( ! AMP_Validation_Manager::has_cap() || ! current_user_can( 'edit_post', $post->ID ) ) { wp_die( esc_html__( 'You do not have permissions to validate an AMP URL. Did you get logged out?', 'amp' ) ); } $updated_count = 0; $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 ( $_POST[ AMP_Validation_Manager::VALIDATION_ERROR_TERM_STATUS_QUERY_VAR ] as $term_slug => $status ) { if ( ! is_numeric( $status ) ) { continue; } $term_slug = sanitize_key( $term_slug ); $term = AMP_Validation_Error_Taxonomy::get_term( $term_slug ); if ( ! $term ) { continue; } $term_group = AMP_Validation_Error_Taxonomy::sanitize_term_status( $status ); if ( null !== $term_group && $term_group !== $term->term_group ) { $updated_count++; wp_update_term( $term->term_id, AMP_Validation_Error_Taxonomy::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 ); } $args = array( 'amp_taxonomy_terms_updated' => $updated_count, ); /* * Re-check the post after the validation status change. This is particularly important for validation errors like * 'removed_unused_css_rules' since whether it is accepted will determine whether other validation errors are triggered * such as in this case 'excessive_css'. */ if ( $updated_count > 0 ) { $validation_results = self::recheck_post( $post->ID ); // @todo For WP_Error case, see . if ( ! is_wp_error( $validation_results ) ) { $args[ self::REMAINING_ERRORS ] = count( array_filter( $validation_results, function( $result ) { return ! $result['sanitized']; } ) ); } } $redirect = wp_get_referer(); if ( ! $redirect ) { $redirect = get_edit_post_link( $post->ID, 'raw' ); } $redirect = remove_query_arg( wp_removable_query_args(), $redirect ); wp_safe_redirect( add_query_arg( $args, $redirect ) ); exit(); } /** * Enqueue scripts for the edit post screen. */ public static function enqueue_edit_post_screen_scripts() { $current_screen = get_current_screen(); if ( 'post' !== $current_screen->base || self::POST_TYPE_SLUG !== $current_screen->post_type ) { return; } // Eliminate autosave since it is only relevant for the content editor. wp_dequeue_script( 'autosave' ); $script_deps_path = AMP__DIR__ . '/assets/js/' . self::EDIT_POST_SCRIPT_HANDLE . '.deps.json'; $script_dependencies = file_exists( $script_deps_path ) ? json_decode( file_get_contents( $script_deps_path ), false ) // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents : array(); wp_enqueue_script( self::EDIT_POST_SCRIPT_HANDLE, amp_get_asset_url( 'js/' . self::EDIT_POST_SCRIPT_HANDLE . '.js' ), $script_dependencies, AMP__VERSION, true ); $current_screen = get_current_screen(); if ( $current_screen && 'post' === $current_screen->base && self::POST_TYPE_SLUG === $current_screen->post_type ) { $post = get_post(); $data = array( 'page_heading' => self::get_single_url_page_heading(), 'amp_enabled' => self::is_amp_enabled_on_post( $post ), ); wp_localize_script( self::EDIT_POST_SCRIPT_HANDLE, 'ampValidation', $data ); } if ( function_exists( 'wp_set_script_translations' ) ) { wp_set_script_translations( self::EDIT_POST_SCRIPT_HANDLE, 'amp' ); } elseif ( function_exists( 'wp_get_jed_locale_data' ) || function_exists( 'gutenberg_get_jed_locale_data' ) ) { $locale_data = function_exists( 'wp_get_jed_locale_data' ) ? wp_get_jed_locale_data( 'amp' ) : gutenberg_get_jed_locale_data( 'amp' ); $translations = wp_json_encode( $locale_data ); wp_add_inline_script( self::EDIT_POST_SCRIPT_HANDLE, 'wp.i18n.setLocaleData( ' . $translations . ', "amp" );', 'after' ); } } /** * Adds the meta boxes to the CPT post.php page. * * @return void */ public static function add_meta_boxes() { remove_meta_box( 'submitdiv', self::POST_TYPE_SLUG, 'side' ); remove_meta_box( 'slugdiv', 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', 'default', array( '__back_compat_meta_box' => true ) ); } /** * 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 ) { ?>
%s', 'amp' ) ), /* translators: Meta box date format */ esc_html( date_i18n( __( 'M j, Y @ H:i', 'amp' ), strtotime( $post->post_date ) ) ) ); ?>

'; echo ''; esc_html_e( 'Stale results', 'amp' ); echo ''; echo '
'; if ( ! empty( $staleness['theme'] ) && ! empty( $staleness['plugins'] ) ) { esc_html_e( 'Different theme and plugins were active when these results were obtained.', 'amp' ); echo ' '; } elseif ( ! empty( $staleness['theme'] ) ) { esc_html_e( 'A different theme was active when these results were obtained.', 'amp' ); echo ' '; } elseif ( ! empty( $staleness['plugins'] ) ) { esc_html_e( 'Different plugins were active when these results were obtained.', 'amp' ); echo ' '; } esc_html_e( 'Please recheck.', 'amp' ); echo '

'; } ?> true ) ); ?>
ID, '_amp_queried_object', true ); if ( isset( $queried_object['id'] ) && isset( $queried_object['type'] ) ) { $after = ' | '; if ( 'post' === $queried_object['type'] && get_post( $queried_object['id'] ) && post_type_exists( get_post( $queried_object['id'] )->post_type ) ) { $post_type_object = get_post_type_object( get_post( $queried_object['id'] )->post_type ); edit_post_link( $post_type_object->labels->edit_item, '', $after, $queried_object['id'] ); $view_label = $post_type_object->labels->view_item; } elseif ( 'term' === $queried_object['type'] && get_term( $queried_object['id'] ) && taxonomy_exists( get_term( $queried_object['id'] )->taxonomy ) ) { $taxonomy_object = get_taxonomy( get_term( $queried_object['id'] )->taxonomy ); edit_term_link( $taxonomy_object->labels->edit_item, '', $after, get_term( $queried_object['id'] ) ); $view_label = $taxonomy_object->labels->view_item; } elseif ( 'user' === $queried_object['type'] ) { $link = get_edit_user_link( $queried_object['id'] ); if ( $link ) { printf( '%s%s', esc_url( $link ), esc_html__( 'Edit User', 'amp' ), esc_html( $after ) ); } $view_label = __( 'View User', 'amp' ); } } printf( '%s', esc_url( self::get_url_from_post( $post ) ), esc_html( $view_label ) ); ?>
post_type ) { return; } $taxonomy = AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG; $taxonomy_object = get_taxonomy( $taxonomy ); if ( ! $taxonomy_object ) { wp_die( esc_html__( 'Invalid taxonomy.', 'amp' ) ); } /** * Set the order of the terms in the order of occurrence. * * Note that this function will call \AMP_Validation_Error_Taxonomy::get_term() repeatedly, and the * object cache will be pre-populated with terms due to the term query in the term list table. * * @return WP_Term[] */ $override_terms_in_occurrence_order = function() use ( $post ) { return wp_list_pluck( AMP_Validated_URL_Post_Type::get_invalid_url_validation_errors( $post ), 'term' ); }; add_filter( 'get_terms', $override_terms_in_occurrence_order ); $wp_list_table = _get_list_table( 'WP_Terms_List_Table' ); get_current_screen()->set_screen_reader_content( array( 'heading_pagination' => $taxonomy_object->labels->items_list_navigation, 'heading_list' => $taxonomy_object->labels->items_list, ) ); $wp_list_table->prepare_items(); $wp_list_table->views(); // The inline script depends on data from the list table. self::$total_errors_for_url = $wp_list_table->get_pagination_arg( 'total_items' ); ?>
search_box( esc_html__( 'Search Errors', 'amp' ), 'invalid-url-search' ); ?>
display(); ?> post_type ) { return; } $url = self::get_url_from_post( $post ); if ( ! $url ) { return; } ?>

', esc_html( $url ), '' ); ?>

base === 'edit' && get_current_screen()->post_type === self::POST_TYPE_SLUG && self::POST_TYPE_SLUG === get_post_type( $id ) ) { $title = preg_replace( '#^(\w+:)?//[^/]+#', '', $title ); } return $title; } /** * Renders the filters on the validated URL post type edit.php page. * * @param string $post_type The slug of the post type. * @param string $which The location for the markup, either 'top' or 'bottom'. */ public static function render_post_filters( $post_type, $which ) { if ( self::POST_TYPE_SLUG === $post_type && 'top' === $which ) { AMP_Validation_Error_Taxonomy::render_error_status_filter(); AMP_Validation_Error_Taxonomy::render_error_type_filter(); } } /** * Gets the URL 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 string|WP_Post $url_or_post The post storing the validation error or the URL to check. * @return string The URL to recheck the post. */ public static function get_recheck_url( $url_or_post ) { $args = array( 'action' => self::VALIDATE_ACTION, ); if ( is_string( $url_or_post ) ) { $args['url'] = $url_or_post; } elseif ( $url_or_post instanceof WP_Post && self::POST_TYPE_SLUG === $url_or_post->post_type ) { $args['post'] = $url_or_post->ID; } return wp_nonce_url( add_query_arg( $args, admin_url() ), self::NONCE_ACTION ); } /** * 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 => array( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_REJECTED_STATUS, AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_ACCEPTED_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 => array( AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_REJECTED_STATUS, AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_ACCEPTED_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' ), number_format_i18n( $query->found_posts ) ) ) ); } return $items; } /** * Print styles for the At a Glance widget. */ public static function print_dashboard_glance_styles() { ?> '. * * @return string|null The page heading, or null. */ public static function get_single_url_page_heading() { global $pagenow; if ( 'post.php' !== $pagenow || ! isset( $_GET['post'], $_GET['action'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended || self::POST_TYPE_SLUG !== get_post_type( $_GET['post'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended ) { return null; } // Mainly uses the same conditionals as print_status_meta_box(). $post = get_post(); $queried_object = get_post_meta( $post->ID, '_amp_queried_object', true ); $name = __( 'Single URL', 'amp' ); // Default. if ( isset( $queried_object['type'], $queried_object['id'] ) ) { if ( 'post' === $queried_object['type'] && get_post( $queried_object['id'] ) ) { $name = html_entity_decode( get_the_title( $queried_object['id'] ), ENT_QUOTES ); } elseif ( 'term' === $queried_object['type'] && get_term( $queried_object['id'] ) ) { $name = get_term( $queried_object['id'] )->name; } elseif ( 'user' === $queried_object['type'] && get_user_by( 'ID', $queried_object['id'] ) ) { $name = get_user_by( 'ID', $queried_object['id'] )->display_name; } } /* translators: %s is the name of the page with the the validation error(s) */ return esc_html( sprintf( __( 'Errors for: %s', 'amp' ), $name ) ); } /** * Filters post row actions. * * Manages links for details, recheck, view, forget, and forget permanently. * * @param array $actions Row action links. * @param \WP_Post $post Current WP post. * @return array Filtered action links. */ public static function filter_post_row_actions( $actions, $post ) { if ( ! is_object( $post ) || self::POST_TYPE_SLUG !== $post->post_type ) { return $actions; } // Inline edits are not relevant. unset( $actions['inline hide-if-no-js'] ); if ( isset( $actions['edit'] ) ) { $actions['edit'] = sprintf( '%s', esc_url( get_edit_post_link( $post ) ), esc_html__( 'Details', 'amp' ) ); } if ( 'trash' !== $post->post_status && current_user_can( 'edit_post', $post->ID ) ) { $url = self::get_url_from_post( $post ); if ( $url ) { $actions['view'] = sprintf( '%s', esc_url( add_query_arg( AMP_Validation_Manager::VALIDATE_QUERY_VAR, '', $url ) ), esc_html__( 'View', 'amp' ) ); } $actions[ self::VALIDATE_ACTION ] = sprintf( '%s', esc_url( self::get_recheck_url( $post ) ), esc_html__( 'Recheck', 'amp' ) ); if ( self::get_post_staleness( $post ) ) { $actions[ self::VALIDATE_ACTION ] = sprintf( '%s', $actions[ self::VALIDATE_ACTION ] ); } } // Replace 'Trash' with 'Forget' (which permanently deletes). $has_delete = ( isset( $actions['trash'] ) || isset( $actions['delete'] ) ); unset( $actions['trash'], $actions['delete'] ); if ( $has_delete ) { $actions['delete'] = sprintf( '%s', get_delete_post_link( $post->ID, '', true ), /* translators: %s: post title */ esc_attr( sprintf( __( 'Forget “%s”', 'amp' ), self::get_url_from_post( $post ) ) ), esc_html__( 'Forget', 'amp' ) ); } return $actions; } /** * Filters table views for the post type. * * @param array $views Array of table view links keyed by status slug. * @return array Filtered views. */ public static function filter_table_views( $views ) { // Replace 'Trash' text with 'Forgotten'. if ( isset( $views['trash'] ) ) { $status = get_post_status_object( 'trash' ); $views['trash'] = str_replace( $status->label, esc_html__( 'Forgotten', 'amp' ), $views['trash'] ); } return $views; } /** * Filters messages displayed after bulk updates. * * Note that trashing is replaced with deletion whenever possible, so the trashed and untrashed messages will not be used in practice. * * @param array $messages Bulk message text. * @param array $bulk_counts Post numbers for the current message. * @return array Filtered messages. */ public static function filter_bulk_post_updated_messages( $messages, $bulk_counts ) { if ( get_current_screen()->id === sprintf( 'edit-%s', self::POST_TYPE_SLUG ) ) { $messages['post'] = array_merge( $messages['post'], array( /* translators: %s is the number of posts forgotten */ 'deleted' => _n( '%s validated URL forgotten.', '%s validated URLs forgotten.', $bulk_counts['deleted'], 'amp' ), /* translators: %s is the number of posts forgotten */ 'trashed' => _n( '%s validated URL forgotten.', '%s validated URLs forgotten.', $bulk_counts['trashed'], 'amp' ), /* translators: %s is the number of posts restored from trash. */ 'untrashed' => _n( '%s validated URL unforgotten.', '%s validated URLs unforgotten.', $bulk_counts['untrashed'], 'amp' ), ) ); } return $messages; } /** * Is AMP Enabled on Post * * @param WP_Post $post Post object to check. * * @return bool|void */ public static function is_amp_enabled_on_post( $post ) { if ( empty( $post ) ) { return; } $validation_errors = self::get_invalid_url_validation_errors( $post ); $counts = self::count_invalid_url_validation_errors( $validation_errors ); $are_there_unaccepted_errors = ( $counts['new_rejected'] || $counts['ack_rejected'] ); return ! $are_there_unaccepted_errors; } /** * Count URL Validation Errors * * @param array $validation_errors Validation errors. * * @return array */ protected static function count_invalid_url_validation_errors( $validation_errors ) { $counts = array_fill_keys( array( 'new_accepted', 'ack_accepted', 'new_rejected', 'ack_rejected' ), 0 ); foreach ( $validation_errors as $error ) { switch ( $error['term']->term_group ) { case AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_REJECTED_STATUS: $counts['new_rejected']++; break; case AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_ACCEPTED_STATUS: $counts['new_accepted']++; break; case AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACK_ACCEPTED_STATUS: $counts['ack_accepted']++; break; case AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACK_REJECTED_STATUS: $counts['ack_rejected']++; break; } } return $counts; } }