value to not filter at all, like for 'All Statuses'. * * This is also used in WP_List_Table, like for the 'Bulk Actions' option. * When this is present, this ensures that this isn't filtered. * * @var string */ const NO_FILTER_VALUE = ''; /** * The 'type' of error for invalid HTML elements, like . * * These usually have the 'code' of AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG. * Except for AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG errors for a cap->edit_posts ) ) { return; } $id = 'link-errors-url'; printf( '', esc_url( add_query_arg( 'post_type', AMP_Validated_URL_Post_Type::POST_TYPE_SLUG, admin_url( 'edit.php' ) ) ), esc_attr( $id ), esc_html__( 'View Validated URLs', 'amp' ) ); ?> element. * * There is a difference how the errors are counted, depending on which screen this is on. * For example: Removed Markup (10). * This status filter 'edit' === $screen_base ? _x( 'With unreviewed errors', 'terms', 'amp' ) : _x( 'Unreviewed errors', 'terms', 'amp' ), 'value' => self::VALIDATION_ERROR_NEW_REJECTED_STATUS . ',' . self::VALIDATION_ERROR_NEW_ACCEPTED_STATUS, 'error_count' => $unreviewed_count, ], [ 'text' => 'edit' === $screen_base ? _x( 'With removed markup', 'terms', 'amp' ) : _x( 'Removed markup', 'terms', 'amp' ), 'value' => self::VALIDATION_ERROR_NEW_ACCEPTED_STATUS . ',' . self::VALIDATION_ERROR_ACK_ACCEPTED_STATUS, 'error_count' => $accepted_term_count, ], [ 'text' => 'edit' === $screen_base ? _x( 'With kept markup', 'terms', 'amp' ) : _x( 'Kept markup', 'terms', 'amp' ), 'value' => self::VALIDATION_ERROR_NEW_REJECTED_STATUS . ',' . self::VALIDATION_ERROR_ACK_REJECTED_STATUS, 'error_count' => $rejected_term_count, ], ]; foreach ( $options_config as $option_config ) { self::output_error_status_filter_option_markup( $option_config['text'], $option_config['value'], $option_config['error_count'], $error_status_filter_value ); } ?> %s (%s)', esc_attr( $option_value ), selected( $selected_value, $option_value, false ), esc_html( $option_text ), esc_html( number_format_i18n( $error_count ) ) ); } /** * Gets all of the possible error types. * * @return string[] Error types. */ public static function get_error_types() { return [ self::HTML_ELEMENT_ERROR_TYPE, self::HTML_ATTRIBUTE_ERROR_TYPE, self::JS_ERROR_TYPE, self::CSS_ERROR_TYPE ]; } /** * Renders the filter for error type. * * This type filter get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_taxonomy WHERE taxonomy = %s AND count = 0", self::TAXONOMY_SLUG ) ); if ( $count > 0 ) { wp_nonce_field( self::VALIDATION_ERROR_CLEAR_EMPTY_ACTION, self::VALIDATION_ERROR_CLEAR_EMPTY_ACTION . '_nonce', false ); submit_button( __( 'Clear Empty', 'amp' ), '', self::VALIDATION_ERROR_CLEAR_EMPTY_ACTION, false ); } } /** * 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_Validated_URL_Post_Type::POST_TYPE_SLUG === get_current_screen()->post_type ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } // Show success messages for accepting/rejecting validation errors. if ( ! empty( $_GET['amp_actioned'] ) && ! empty( $_GET['amp_actioned_count'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $actioned = sanitize_key( $_GET['amp_actioned'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $count = (int) $_GET['amp_actioned_count']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $message = null; if ( 'delete' === $actioned ) { $message = sprintf( /* translators: %s is number of errors accepted */ _n( 'Deleted %s instance of validation errors.', 'Deleted %s instances of validation errors.', number_format_i18n( $count ), 'amp' ), $count ); } if ( $message ) { printf( '

%s

', esc_html( $message ) ); } } // Show success message for clearing empty terms. if ( isset( $_GET[ self::VALIDATION_ERRORS_CLEARED_QUERY_VAR ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $cleared_count = (int) $_GET[ self::VALIDATION_ERRORS_CLEARED_QUERY_VAR ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended printf( '

%s

', esc_html( sprintf( /* translators: %s is the number of validation errors cleared */ _n( 'Cleared %s validation error for invalid markup that no longer occurs on the site.', 'Cleared %s validation errors for invalid markup that no longer occur on the site.', $cleared_count, 'amp' ), number_format_i18n( $cleared_count ) ) ) ); } } /** * Get the validation data and source info from the validated URL if available (as it should be). * * @param WP_Term $term Term. * @return array|null Validation data if successfully retrieved from the validated URL post, or else null. */ private static function get_current_validation_error_data_from_post( WP_Term $term ) { $post = get_post(); if ( ! $post instanceof WP_Post || AMP_Validated_URL_Post_Type::POST_TYPE_SLUG !== $post->post_type ) { return null; } $validation_errors = AMP_Validated_URL_Post_Type::get_invalid_url_validation_errors( $post ); if ( ! isset( $validation_errors[ self::$current_validation_error_row_index ] ) ) { return null; } $validation_error = $validation_errors[ self::$current_validation_error_row_index ]; if ( $term->term_id !== $validation_error['term']->term_id ) { return null; } return $validation_error['data']; } /** * Returns JSON-formatted error details for an error term. * * @param WP_Term $term Term. * @return string Encoded JSON. */ public static function get_error_details_json( WP_Term $term ) { $validation_data = self::get_current_validation_error_data_from_post( $term ); // Fall back to obtaining the validation data from the term itself (without sources). if ( null === $validation_data ) { $validation_data = json_decode( $term->description, true ); } // Total failure to obtain the validation data. if ( ! is_array( $validation_data ) ) { return wp_json_encode( [] ); } // Convert the numeric constant value of the node_type to its constant name. $xml_reader_reflection_class = new ReflectionClass( 'XMLReader' ); $constants = $xml_reader_reflection_class->getConstants(); foreach ( $constants as $key => $value ) { if ( $validation_data['node_type'] === $value ) { $validation_data['node_type'] = $key; break; } } $validation_data['removed'] = (bool) ( (int) $term->term_group & self::ACCEPTED_VALIDATION_ERROR_BIT_MASK ); $validation_data['reviewed'] = (bool) ( (int) $term->term_group & self::ACKNOWLEDGED_VALIDATION_ERROR_BIT_MASK ); return wp_json_encode( $validation_data ); } /** * Reset the index for the current validation error being displayed. */ public static function reset_validation_error_row_index() { self::$current_validation_error_row_index = 0; } /** * 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 ) { global $pagenow; $term_id = $tag->term_id; $term = get_term( $term_id ); // We don't want filter=display given by $tag. /* * Hide deletion link since a validation error should only be removed once * it no longer has an occurrence on the site. When a validated URL is re-checked * and it no longer has this validation error, then the count will be decremented. * When a validation error term no longer has a count, then it is hidden from the * list table. A cron job could periodically delete terms that have no counts. */ unset( $actions['delete'] ); if ( 'post.php' === $pagenow ) { $actions['details'] = sprintf( '', esc_attr__( 'Toggle error details', 'amp' ), esc_html__( 'Details', 'amp' ) ); $actions['copy'] = sprintf( '', esc_attr( self::get_error_details_json( $term ) ), esc_html__( 'Copy to clipboard', 'amp' ) ); } elseif ( 'edit-tags.php' === $pagenow ) { $actions['details'] = sprintf( '%s', admin_url( add_query_arg( [ self::TAXONOMY_SLUG => $term->name, 'post_type' => AMP_Validated_URL_Post_Type::POST_TYPE_SLUG, ], 'edit.php' ) ), esc_html__( 'Details', 'amp' ) ); if ( 0 === $term->count ) { $actions['delete'] = sprintf( '%s', wp_nonce_url( add_query_arg( array_merge( [ 'action' => 'delete' ], compact( 'term_id' ) ) ), 'delete' ), esc_html__( 'Delete', 'amp' ) ); } } $actions = wp_array_slice_assoc( $actions, [ 'details', 'delete', 'copy' ] ); self::$current_validation_error_row_index++; return $actions; } /** * Show AMP validation errors under AMP admin menu. */ public static function add_admin_menu_validation_error_item() { $menu_item_label = esc_html__( 'Error Index', 'amp' ); if ( ValidationCounts::is_needed() ) { // Append markup to display a loading spinner while the unreviewed count is being fetched. $menu_item_label .= ' '; } $post_menu_slug = 'edit.php?post_type=' . AMP_Validated_URL_Post_Type::POST_TYPE_SLUG; $term_menu_slug = 'edit-tags.php?taxonomy=' . self::TAXONOMY_SLUG . '&post_type=' . AMP_Validated_URL_Post_Type::POST_TYPE_SLUG; global $submenu; if ( current_user_can( 'manage_options' ) ) { $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, $menu_item_label, $menu_item_label, $taxonomy_caps->manage_terms, // The following esc_attr() is sadly needed due to . esc_attr( $term_menu_slug ) ); } elseif ( isset( $submenu[ $post_menu_slug ] ) ) { foreach ( $submenu[ $post_menu_slug ] as &$submenu_item ) { if ( esc_attr( $term_menu_slug ) === $submenu_item[2] ) { $submenu_item[0] = $menu_item_label; } } } } /** * Provides a reader-friendly string for a term's error type. * * @param string $error_type The error type from the term's validation error JSON. * @return string Reader-friendly string. */ public static function get_reader_friendly_error_type_text( $error_type ) { switch ( $error_type ) { case 'js_error': return esc_html__( 'JS', 'amp' ); case 'html_element_error': return esc_html__( 'HTML element', 'amp' ); case 'html_attribute_error': return esc_html__( 'HTML attribute', 'amp' ); case 'css_error': return esc_html__( 'CSS', 'amp' ); default: return $error_type; } } /** * Provides the label for the details summary element. * * @param array $validation_error Validation error data. * @return string The label. */ public static function get_details_summary_label( $validation_error ) { $error_type = isset( $validation_error['type'] ) ? $validation_error['type'] : null; $node_type = isset( $validation_error['node_type'] ) ? $validation_error['node_type'] : null; if ( self::CSS_ERROR_TYPE === $error_type ) { $summary_label = sprintf( '<%s>', $validation_error['node_name'] ); } elseif ( isset( $validation_error['parent_name'] ) ) { $summary_label = sprintf( '<%s>', $validation_error['parent_name'] ); } elseif ( isset( $validation_error['node_name'] ) && XML_ELEMENT_NODE === $node_type ) { $summary_label = sprintf( '<%s>', $validation_error['node_name'] ); } else { $summary_label = '…'; } return sprintf( '%s', esc_html( $summary_label ) ); } /** * 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 ) { global $pagenow; $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_code': if ( 'post.php' === $pagenow ) { $content .= sprintf( ''; } else { $content .= ''; $content .= '

'; } $message = null; switch ( $validation_error['code'] ) { case AMP_Tag_And_Attribute_Sanitizer::JSON_ERROR_EMPTY: $message = __( 'Expected JSON, got an empty value', 'amp' ); break; case AMP_Tag_And_Attribute_Sanitizer::JSON_ERROR_DEPTH: $message = __( 'The maximum stack depth has been exceeded', 'amp' ); break; case AMP_Tag_And_Attribute_Sanitizer::JSON_ERROR_STATE_MISMATCH: $message = __( 'Invalid or malformed JSON', 'amp' ); break; case AMP_Tag_And_Attribute_Sanitizer::JSON_ERROR_CTRL_CHAR: $message = __( 'Control character error, possibly incorrectly encoded', 'amp' ); break; case AMP_Tag_And_Attribute_Sanitizer::JSON_ERROR_SYNTAX: $message = __( 'Syntax error', 'amp' ); break; case AMP_Tag_And_Attribute_Sanitizer::JSON_ERROR_UTF8: /* translators: %s: UTF-8, a charset */ $message = sprintf( __( 'Malformed %s characters, possibly incorrectly encoded', 'amp' ), 'UTF-8' ); break; default: if ( isset( $validation_error['message'] ) ) { $message = $validation_error['message']; } } if ( $message ) { $content .= sprintf( '

%s

', esc_html( $message ) ); } break; case 'status': // Output whether the validation error has been seen via hidden field since we can't set the 'new' class on the directly. // This will get read via amp-validated-url-post-edit-screen.js. $is_new = ! ( (int) $term->term_group & self::ACKNOWLEDGED_VALIDATION_ERROR_BIT_MASK ); $content .= sprintf( '', (int) $is_new ); $is_removed = (bool) ( (int) $term->term_group & self::ACCEPTED_VALIDATION_ERROR_BIT_MASK ); if ( 'post.php' === $pagenow ) { $status_select_name = sprintf( '%s[%s][%s]', AMP_Validated_URL_Post_Type::VALIDATION_ERRORS_INPUT_KEY, $term->slug, AMP_Validation_Manager::VALIDATION_ERROR_TERM_STATUS_QUERY_VAR ); ob_start(); ?>
', (int) $is_removed ); } 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' ); } else { $time_ago = sprintf( '%s', esc_attr( $created_datetime->format( /* translators: localized date and time format, see http://php.net/date */ __( 'F j, Y g:i a', 'amp' ) ) ), /* translators: %s: the human-readable time difference. */ esc_html( sprintf( __( '%s ago', 'amp' ), human_time_diff( $created_datetime->getTimestamp() ) ) ) ); } if ( $created_datetime ) { $time_ago = sprintf( '', $created_datetime->format( 'c' ), $time_ago ); } $content .= $time_ago; break; case 'details': if ( 'post.php' === $pagenow ) { return self::render_single_url_error_details( $validation_error, $term ); } if ( isset( $validation_error['parent_name'] ) ) { $summary = self::get_details_summary_label( $validation_error ); unset( $validation_error['error_type'], $validation_error['parent_name'] ); $attributes = []; $attributes_heading = ''; if ( ! empty( $validation_error['node_attributes'] ) ) { $attributes_heading = sprintf( '
%s:
', esc_html( self::get_source_key_label( 'node_attributes', $validation_error ) ) ); $attributes = $validation_error['node_attributes']; } elseif ( ! empty( $validation_error['element_attributes'] ) ) { $attributes = $validation_error['element_attributes']; $attributes_heading = sprintf( '
%s:
', esc_html( self::get_source_key_label( 'element_attributes', $validation_error ) ) ); } if ( empty( $attributes ) ) { $content .= $summary; } else { $content = '
'; $content .= ''; $content .= $summary; $content .= ''; $content .= $attributes_heading; $content .= ''; $content .= '
'; } } break; case 'sources_with_invalid_output': if ( ! isset( $_GET['post'], $_GET['action'] ) || 'edit' !== $_GET['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended break; } $url_post_id = (int) $_GET['post']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $validation_errors = AMP_Validated_URL_Post_Type::get_invalid_url_validation_errors( $url_post_id ); $validation_errors = array_filter( $validation_errors, static function( $error ) use ( $term ) { return $error['term']->term_id === $term->term_id; } ); $error_summary = self::summarize_validation_errors( wp_list_pluck( $validation_errors, 'data' ) ); if ( empty( $error_summary[ self::SOURCES_INVALID_OUTPUT ] ) ) { esc_html_e( '--', 'amp' ); } else { AMP_Validated_URL_Post_Type::render_sources_column( $error_summary[ self::SOURCES_INVALID_OUTPUT ], $url_post_id ); } break; case 'error_type': if ( isset( $validation_error['type'] ) ) { $text = self::get_reader_friendly_error_type_text( $validation_error['type'] ); if ( 'post.php' === $pagenow ) { $content .= sprintf( '

%s

', isset( $validation_error['type'] ) ? $validation_error['type'] : '', esc_html( $text ) ); } else { $content .= $text; } } else { $content .= esc_html__( 'Misc', 'amp' ); } break; case 'reviewed': if ( 'post.php' === $pagenow ) { $checked = checked( 0 < ( (int) $term->term_group & self::ACKNOWLEDGED_VALIDATION_ERROR_BIT_MASK ), true, false ); $input_name = sprintf( '%s[%s][%s]', AMP_Validated_URL_Post_Type::VALIDATION_ERRORS_INPUT_KEY, $term->slug, self::VALIDATION_ERROR_ACKNOWLEDGE_ACTION ); $content .= sprintf( '', esc_attr( $input_name ), $checked ); } } return $content; } /** * Adds post columns to the /wp-admin/post.php page for amp_validated_url. * * @param array $sortable_columns The sortable columns. * @return array $sortable_columns The filtered sortable columns. */ public static function add_single_post_sortable_columns( $sortable_columns ) { return array_merge( $sortable_columns, [ 'error_code' => self::VALIDATION_DETAILS_ERROR_CODE_QUERY_VAR, 'error_type' => self::VALIDATION_ERROR_TYPE_QUERY_VAR, ] ); } /** * Renders error details when viewing a single URL page. * * @param array $validation_error Validation error data. * @param WP_Term $term The validation error term. * @param bool $wrap_with_details Whether to wrap the error details markup with a
element. * @param bool $with_summary Whether to include the summary for the
element. * @return string HTML for the details section. */ public static function render_single_url_error_details( $validation_error, $term, $wrap_with_details = true, $with_summary = true ) { // Get the sources, if they exist. if ( isset( $_GET['post'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $validation_errors = AMP_Validated_URL_Post_Type::get_invalid_url_validation_errors( (int) $_GET['post'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended foreach ( $validation_errors as $error ) { if ( isset( $error['data']['sources'], $error['term']->term_id ) && $error['term']->term_id === $term->term_id ) { $validation_error['sources'] = $error['data']['sources']; break; } } } ob_start(); ?>

AMP components, which are added automatically by the AMP plugin. For any page to be served as AMP, all invalid script tags must be removed from the page. Instead of custom or third-party JS, please consider using AMP components and functionality such as %7$s and actions and events (as opposed to JS event handler attributes like %5$s). Some custom JS can be added if encapsulated in the %9$s. Learn more about how AMP works.', 'amp' ), '<script>', 'https://amp.dev/documentation/components/', 'https://amp.dev/about/how-amp-works/', 'https://amp.dev/documentation/guides-and-tutorials/learn/amp-actions-and-events/', 'onclick', 'https://amp.dev/documentation/components/amp-bind/', 'amp-bind', 'https://amp.dev/documentation/components/amp-script/', 'amp-script' ) ) ?> style your pages using CSS in much the same way as regular HTML pages, however there are some restrictions. Nevertheless, the AMP plugin automatically inlines external stylesheets, transforms %3$s qualifiers, and uses tree shaking to remove the majority of CSS rules that do not apply to the current page. Nevertheless, AMP does have a 75KB limit and tree shaking cannot always reduce the amount of CSS under this limit; when this happens an excessive CSS error will result.', 'amp' ), 'https://amp.dev/documentation/guides-and-tutorials/develop/style_and_layout/', 'https://amp.dev/documentation/guides-and-tutorials/develop/style_and_layout/style_pages/', '!important' ) ) ?> AMP HTML specification. If an element or attribute is not allowed in AMP, it must be removed for the page to be cached and eligible for prerendering.', 'amp' ), 'https://amp.dev/documentation/guides-and-tutorials/learn/spec/amphtml/', 'https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors/how_amp_pages_are_cached/' ) ) ?>

0 ) { echo ' … '; } echo '>'; ?>
1 ) { echo ' …'; } echo ''; printf( ' %s="%s"', esc_html( $validation_error['node_name'] ), esc_html( $validation_error['element_attributes'][ $validation_error['node_name'] ] ) ); echo ''; if ( count( $validation_error['element_attributes'] ) > 1 ) { echo ' …'; } echo '>'; ?>
$value ) : ?>
%s: %s', esc_html( $value ), esc_html( $validation_error['css_property_value'] ) ); } else { printf( '%s', esc_html( $value ) ); } ?> %s', esc_html( $value ) ); if ( isset( $validation_error['meta_property_value'] ) ) { printf( ': %s', esc_html( $validation_error['meta_property_value'] ) ); } if ( isset( $validation_error['meta_property_required_value'] ) ) { printf( ' (%s: %s)', esc_html__( 'required value', 'amp' ), esc_html( $validation_error['meta_property_required_value'] ) ); } ?>
    %s', esc_html( $attr ) ); ?>
$attr_value ) : ?> %2$s', esc_attr( $attr_name_class ), esc_html( $attr_name ) ); echo ''; ?>
'; if ( ! empty( $attr_value ) ) { echo ''; $is_url = in_array( $attr_name, [ 'href', 'src' ], true ); if ( $is_url ) { // Remove non-helpful normalized version. $url_query = wp_parse_url( $attr_value, PHP_URL_QUERY ); if ( $url_query && false !== strpos( 'ver=__normalized__', $url_query ) ) { $attr_value = remove_query_arg( 'ver', $attr_value ); } printf( '', esc_url( $attr_value ) ); } echo esc_html( $attr_value ); if ( $is_url ) { echo ''; } echo ''; } echo '
$attr ) : ?> %s', esc_html( $value_key ) ); if ( ! empty( $attr ) ) { echo ': '; echo esc_html( $attr ); } } ?>
%s%s', self::get_details_summary_label( $validation_error ), $output ); } if ( $wrap_with_details ) { $output = '
' . $output . '
'; } return $output; } /** * Get the URL for opening the file for a AMP validation error in an external editor. * * @since 1.4 * * @param array $source Source for AMP validation error. * @return string|null File editor URL or null if not available. */ private static function get_file_editor_url( $source ) { if ( ! isset( $source['file'], $source['line'], $source['type'], $source['name'] ) ) { return null; } $edit_url = null; /** * Filters the template for the URL for linking to an external editor to open a file for editing. * * Users of IDEs that support opening files in via web protocols can use this filter to override * the edit link to result in their editor opening rather than the theme/plugin editor. * * The initial filtered value is null, requiring extension plugins to supply the URL template * string themselves. If no template string is provided, links to the theme/plugin editors will * be provided if available. For example, for an extension plugin to cause file edit links to * open in PhpStorm, the following filter can be used: * * add_filter( 'amp_validation_error_source_file_editor_url_template', function () { * return 'phpstorm://open?file={{file}}&line={{line}}'; * } ); * * For a template to be considered, the string '{{file}}' must be present in the filtered value. * * @since 1.4 * * @param string|null $editor_url_template Editor URL template. */ $editor_url_template = apply_filters( 'amp_validation_error_source_file_editor_url_template', null ); // Supply the file path to the editor template. if ( null !== $editor_url_template && false !== strpos( $editor_url_template, '{{file}}' ) ) { $file_path = null; if ( 'core' === $source['type'] ) { if ( 'wp-includes' === $source['name'] ) { $file_path = ABSPATH . WPINC . '/' . $source['file']; } elseif ( 'wp-admin' === $source['name'] ) { $file_path = ABSPATH . 'wp-admin/' . $source['file']; } } elseif ( 'plugin' === $source['type'] ) { $file_path = WP_PLUGIN_DIR . '/' . $source['name']; if ( $source['name'] !== $source['file'] ) { $file_path .= '/' . $source['file']; } } elseif ( 'mu-plugin' === $source['type'] ) { $file_path = WPMU_PLUGIN_DIR . '/' . $source['name']; } elseif ( 'theme' === $source['type'] ) { $theme = wp_get_theme( $source['name'] ); if ( $theme instanceof WP_Theme && ! $theme->errors() ) { $file_path = $theme->get_stylesheet_directory() . '/' . $source['file']; } } if ( $file_path && file_exists( $file_path ) ) { /** * Filters the file path to be opened in an external editor for a given AMP validation error source. * * This is useful to map the file path from inside of a Docker container or VM to the host machine. * * @since 1.4 * * @param string|null $editor_url_template Editor URL template. * @param array $source Source information. */ $file_path = apply_filters( 'amp_validation_error_source_file_path', $file_path, $source ); if ( $file_path ) { $edit_url = str_replace( [ '{{file}}', '{{line}}', ], [ rawurlencode( $file_path ), rawurlencode( $source['line'] ), ], $editor_url_template ); } } } // Fall back to using the theme/plugin editors if no external editor is offered. if ( ! $edit_url ) { if ( 'plugin' === $source['type'] && current_user_can( 'edit_plugins' ) ) { $plugin_registry = Services::get( 'plugin_registry' ); $plugin = $plugin_registry->get_plugin_from_slug( $source['name'] ); if ( $plugin ) { $file = $source['file']; // Prepend the plugin directory name to the file name as the plugin editor requires. $i = strpos( $plugin['file'], '/' ); if ( false !== $i ) { $file = substr( $plugin['file'], 0, $i ) . '/' . $file; } $edit_url = add_query_arg( [ 'plugin' => rawurlencode( $plugin['file'] ), 'file' => rawurlencode( $file ), 'line' => rawurlencode( $source['line'] ), ], admin_url( 'plugin-editor.php' ) ); } } elseif ( 'theme' === $source['type'] && current_user_can( 'edit_themes' ) ) { $edit_url = add_query_arg( [ 'file' => rawurlencode( $source['file'] ), 'theme' => rawurlencode( $source['name'] ), 'line' => rawurlencode( $source['line'] ), ], admin_url( 'theme-editor.php' ) ); } } return $edit_url; } /** * Render source name. * * @since 1.4 * * @param string $name Name. * @param string $type Type. */ private static function render_source_name( $name, $type ) { $nicename = null; switch ( $type ) { case 'theme': $theme = wp_get_theme( $name ); if ( ! $theme->errors() ) { $nicename = $theme->get( 'Name' ); } break; case 'plugin': $plugin_registry = Services::get( 'plugin_registry' ); $plugin = $plugin_registry->get_plugin_from_slug( $name ); if ( $plugin && ! empty( $plugin['data']['Name'] ) ) { $nicename = $plugin['data']['Name']; } break; } echo ' '; if ( $nicename ) { printf( '%s (%s)', esc_html( $nicename ), esc_html( $name ) ); } else { echo '' . esc_html( $name ) . ''; } } /** * Render sources. * * @param array $sources Sources. */ public static function render_sources( $sources ) { ?>
$source ) : ?> $source['file'] . ':' . $source['line'], 'link_url' => self::get_file_editor_url( $source ), ]; } $is_filter = ! empty( $source['filter'] ); unset( $source_table_rows['filter'] ); $dependency_type = null; if ( isset( $source['dependency_type'] ) ) { $dependency_type = $source['dependency_type']; unset( $source_table_rows['dependency_type'] ); } $priority = null; if ( isset( $source['priority'] ) ) { $priority = $source['priority']; unset( $source_table_rows['priority'] ); } $row_span = count( $source_table_rows ); ?> $key ) : ?>
# '; esc_html_e( 'Theme', 'amp' ); break; case 'plugin': echo ' '; esc_html_e( 'Plugin', 'amp' ); break; case 'mu-plugin': echo ' '; esc_html_e( 'Must-Use Plugin', 'amp' ); break; case 'core': echo ' '; esc_html_e( 'Core', 'amp' ); break; default: echo esc_html( (string) $value ); } ?> wp_add_inline_style( , … ) wp_localize_script( , … ) wp_add_inline_script( , …, 'before' ) wp_add_inline_script( , …, 'after' ) ', // Note that esc_attr() used instead of esc_url() to allow IDE protocols. esc_attr( $value['link_url'] ), // Open link in new window unless the user has filtered the URL to open their system IDE. in_array( wp_parse_url( $value['link_url'], PHP_URL_SCHEME ), [ 'http', 'https' ], true ) ? 'target="_blank"' : '' ); } ?> %s)', esc_html( $block_title ), esc_html( $value ) ); } else { printf( '%s', esc_html( $value ) ); } ?> labels->singular_name ) ) { echo esc_html( $post_type->labels->singular_name ); printf( ' (%s)', esc_html( $value ) ); } else { printf( '%s', esc_html( $value ) ); } ?>
__( 'HTML Element', 'amp' ), self::HTML_ATTRIBUTE_ERROR_TYPE => __( 'HTML Attribute', 'amp' ), self::JS_ERROR_TYPE => __( 'JavaScript', 'amp' ), self::CSS_ERROR_TYPE => __( 'CSS', 'amp' ), ]; if ( isset( $translated_names[ $validation_error['type'] ] ) ) { return $translated_names[ $validation_error['type'] ]; } return null; } /** * Handle inline edit links. */ public static function handle_inline_edit_request() { // Check for necessary arguments. if ( ! isset( $_GET['action'], $_GET['_wpnonce'], $_GET['term_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } // Check if we are on either the taxonomy page or a single error page (which has the post_type argument). if ( self::TAXONOMY_SLUG !== get_current_screen()->taxonomy && ! isset( $_GET['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } // If we have a post_type check that it is the correct one. if ( isset( $_GET['post_type'] ) && AMP_Validated_URL_Post_Type::POST_TYPE_SLUG !== $_GET['post_type'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } $action = sanitize_key( $_GET['action'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended check_admin_referer( $action ); $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; } $referer = wp_get_referer(); $term_id = (int) $_GET['term_id']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $redirect = self::handle_validation_error_update( $referer, $action, [ $term_id ] ); if ( $redirect !== $referer ) { wp_safe_redirect( $redirect ); exit; } } /** * On the single URL page, handles the bulk actions of 'Remove' (formerly 'Accept') and 'Keep' (formerly 'Reject'). * * On /wp-admin/post.php, this handles these bulk actions. * This page is more like an edit-tags.php page, in that it has a WP_Terms_List_Table of amp_validation_error terms. * So this reuses handle_validation_error_update(), which the edit-tags.php page uses. * * @param int $post_id The ID of the post for which to apply the bulk action. */ public static function handle_single_url_page_bulk_and_inline_actions( $post_id ) { if ( ! isset( $_REQUEST['action'] ) || AMP_Validated_URL_Post_Type::POST_TYPE_SLUG !== get_post_type( $post_id ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } $action = sanitize_key( $_REQUEST['action'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $term_ids = isset( $_POST['delete_tags'] ) ? array_filter( array_map( 'intval', (array) $_POST['delete_tags'] ) ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Missing $single_term_id = isset( $_GET['term_id'] ) ? (int) $_GET['term_id'] : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $redirect_query_args = [ 'action' => 'edit', 'amp_actioned' => $action, ]; if ( $term_ids ) { // If this is a bulk action. self::handle_validation_error_update( null, $action, $term_ids ); $redirect_query_args['amp_actioned_count'] = count( $term_ids ); } elseif ( $single_term_id ) { // If this is an inline action, like 'Details' or 'Delete'. self::handle_validation_error_update( null, $action, [ $single_term_id ] ); $redirect_query_args['amp_actioned_count'] = 1; } // Even if the user didn't select any errors to bulk edit, redirect back to the same page. wp_safe_redirect( add_query_arg( $redirect_query_args, get_edit_post_link( $post_id, 'raw' ) ) ); exit(); } /** * 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 ) { if ( 'delete' !== $action ) { return $redirect_to; } global $pagenow; $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 ); } $updated_count = 0; foreach ( $term_ids as $term_id ) { if ( 'delete' === $action && self::delete_empty_term( $term_id ) ) { $updated_count++; } } if ( false !== $has_pre_term_description_filter ) { add_filter( 'pre_term_description', 'wp_filter_kses', $has_pre_term_description_filter ); } $term_ids_count = count( $term_ids ); if ( 'edit.php' === $pagenow && 'delete' === $action && 1 === $updated_count ) { // Redirect to error index screen if deleting an validation error with no associated validated URLs. $redirect_to = add_query_arg( [ 'amp_actioned' => $action, 'amp_actioned_count' => $term_ids_count, ], esc_url( get_admin_url( null, 'edit-tags.php?taxonomy=' . self::TAXONOMY_SLUG . '&post_type=' . AMP_Validated_URL_Post_Type::POST_TYPE_SLUG ) ) ); } else { $redirect_to = add_query_arg( [ 'amp_actioned' => $action, 'amp_actioned_count' => $term_ids_count, ], $redirect_to ); } if ( $updated_count ) { delete_transient( AMP_Validated_URL_Post_Type::NEW_VALIDATION_ERROR_URLS_COUNT_TRANSIENT ); } return $redirect_to; } /** * Handle request to delete empty terms. */ public static function handle_clear_empty_terms_request() { if ( ! isset( $_POST[ self::VALIDATION_ERROR_CLEAR_EMPTY_ACTION ], $_POST[ self::VALIDATION_ERROR_CLEAR_EMPTY_ACTION . '_nonce' ] ) ) { return; } if ( ! check_ajax_referer( self::VALIDATION_ERROR_CLEAR_EMPTY_ACTION, self::VALIDATION_ERROR_CLEAR_EMPTY_ACTION . '_nonce', false ) ) { wp_die( esc_html__( 'The link you followed has expired.', 'amp' ) ); } $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 ) ) { wp_die( esc_html__( 'You do not have authorization.', 'amp' ) ); } $deleted_terms = self::delete_empty_terms(); $referer = wp_validate_redirect( wp_get_raw_referer() ); if ( ! $referer ) { return; } $redirect = add_query_arg( self::VALIDATION_ERRORS_CLEARED_QUERY_VAR, $deleted_terms, $referer ); wp_safe_redirect( $redirect ); exit; } /** * Determine whether a validation error is for a JS script element. * * @param array $validation_error Validation error. * @return bool Is for scrip JS element. */ private static function is_validation_error_for_js_script_element( $validation_error ) { return ( isset( $validation_error['node_name'] ) && 'script' === $validation_error['node_name'] && ( isset( $validation_error['node_attributes']['src'] ) || empty( $validation_error['node_attributes']['type'] ) || false !== strpos( $validation_error['node_attributes']['type'], 'javascript' ) ) ); } /** * Get Error Title from Code * * @todo The message here should be constructed in the sanitizer that emitted the validation error in the first place. * * @param array $validation_error Validation error. * @return string Title with some formatting markup. */ public static function get_error_title_from_code( $validation_error ) { switch ( $validation_error['code'] ) { case AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG: if ( self::is_validation_error_for_js_script_element( $validation_error ) ) { if ( isset( $validation_error['node_attributes']['src'] ) ) { $title = esc_html__( 'Invalid script', 'amp' ); $basename = basename( wp_parse_url( $validation_error['node_attributes']['src'], PHP_URL_PATH ) ); if ( $basename ) { $title .= sprintf( ': %s', esc_html( $basename ) ); } } else { $title = esc_html__( 'Invalid inline script', 'amp' ); } } else { $title = esc_html__( 'Invalid element', 'amp' ); $title .= sprintf( ': <%s>', esc_html( $validation_error['node_name'] ) ); } return $title; case AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR: return sprintf( '%s: %s', esc_html__( 'Invalid attribute', 'amp' ), esc_html( $validation_error['node_name'] ) ); case AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_PROCESSING_INSTRUCTION: return sprintf( '%s: <%s%s…%s>', esc_html__( 'Invalid processing instruction', 'amp' ), '?', esc_html( $validation_error['node_name'] ), '?' ); case AMP_Style_Sanitizer::STYLESHEET_TOO_LONG: return esc_html__( 'Excessive CSS', 'amp' ); case AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_AT_RULE: return sprintf( '%s: @%s', esc_html__( 'Illegal CSS at-rule', 'amp' ), esc_html( $validation_error['at_rule'] ) ); case AMP_Tag_And_Attribute_Sanitizer::DUPLICATE_UNIQUE_TAG: return sprintf( '%s: <%s>', esc_html__( 'Duplicate element', 'amp' ), esc_html( $validation_error['node_name'] ) ); case AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_DECLARATION: return esc_html__( 'Unrecognized CSS', 'amp' ); case AMP_Style_Sanitizer::CSS_SYNTAX_PARSE_ERROR: return esc_html__( 'CSS parse error', 'amp' ); case AMP_Style_Sanitizer::STYLESHEET_FETCH_ERROR: return esc_html__( 'Stylesheet fetch error', 'amp' ); case AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_PROPERTY: case AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_PROPERTY_NOLIST: $title = esc_html__( 'Illegal CSS property', 'amp' ); if ( isset( $validation_error['css_property_name'] ) ) { $title .= sprintf( ': %s', esc_html( $validation_error['css_property_name'] ) ); } return $title; case AMP_Style_Sanitizer::CSS_DISALLOWED_SELECTOR: return esc_html__( 'Illegal CSS selector', 'amp' ); case AMP_Style_Sanitizer::DISALLOWED_ATTR_CLASS_NAME: $title = esc_html__( 'Disallowed class name', 'amp' ); if ( isset( $validation_error['class_name'] ) ) { $title .= sprintf( ': %s', esc_html( $validation_error['class_name'] ) ); } return $title; case AMP_Tag_And_Attribute_Sanitizer::CDATA_TOO_LONG: case AMP_Tag_And_Attribute_Sanitizer::MANDATORY_CDATA_MISSING_OR_INCORRECT: case AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_HTML_COMMENTS: case AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_CSS_I_AMPHTML_NAME: case AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_CONTENTS: case AMP_Tag_And_Attribute_Sanitizer::CDATA_VIOLATES_DENYLIST: return esc_html__( 'Illegal text content', 'amp' ); case AMP_Tag_And_Attribute_Sanitizer::JSON_ERROR_CTRL_CHAR: case AMP_Tag_And_Attribute_Sanitizer::JSON_ERROR_DEPTH: case AMP_Tag_And_Attribute_Sanitizer::JSON_ERROR_EMPTY: case AMP_Tag_And_Attribute_Sanitizer::JSON_ERROR_STATE_MISMATCH: case AMP_Tag_And_Attribute_Sanitizer::JSON_ERROR_SYNTAX: case AMP_Tag_And_Attribute_Sanitizer::JSON_ERROR_UTF8: return esc_html__( 'Invalid JSON', 'amp' ); case AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_IMPORTANT: $title = esc_html__( 'Illegal CSS !important property', 'amp' ); if ( isset( $validation_error['css_property_name'] ) ) { $title .= sprintf( ': %s', esc_html( $validation_error['css_property_name'] ) ); } return $title; case AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_PROPERTY_IN_ATTR_VALUE: $title = esc_html__( 'Invalid property', 'amp' ); if ( isset( $validation_error['meta_property_name'] ) ) { $title .= sprintf( ': %s', esc_html( $validation_error['meta_property_name'] ) ); } return $title; case AMP_Tag_And_Attribute_Sanitizer::MISSING_MANDATORY_PROPERTY: $title = esc_html__( 'Missing required property', 'amp' ); if ( isset( $validation_error['meta_property_name'] ) ) { $title .= sprintf( ': %s', esc_html( $validation_error['meta_property_name'] ) ); } return $title; case AMP_Tag_And_Attribute_Sanitizer::MISSING_REQUIRED_PROPERTY_VALUE: $title = sprintf( /* translators: %1$s is the property name, %2$s is the value for the property */ esc_html__( 'Invalid value for %1$s property: %2$s', 'amp' ), '' . esc_html( $validation_error['meta_property_name'] ) . '', '' . esc_html( $validation_error['meta_property_value'] ) . '' ); return $title; case AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING: $title = esc_html__( 'Missing required attribute', 'amp' ); if ( isset( $validation_error['attributes'][0] ) ) { $title .= sprintf( ': %s', esc_html( $validation_error['attributes'][0] ) ); } return $title; case AMP_Tag_And_Attribute_Sanitizer::DUPLICATE_ONEOF_ATTRS: $title = esc_html__( 'Mutually exclusive attributes encountered', 'amp' ); if ( ! empty( $validation_error['duplicate_oneof_attrs'] ) ) { $title .= ': '; $title .= implode( ', ', array_map( static function ( $attribute_name ) { return sprintf( '%s', $attribute_name ); }, $validation_error['duplicate_oneof_attrs'] ) ); } return $title; case AMP_Tag_And_Attribute_Sanitizer::MANDATORY_ONEOF_ATTR_MISSING: case AMP_Tag_And_Attribute_Sanitizer::MANDATORY_ANYOF_ATTR_MISSING: $attributes_key = null; if ( AMP_Tag_And_Attribute_Sanitizer::MANDATORY_ONEOF_ATTR_MISSING === $validation_error['code'] ) { $title = esc_html__( 'Missing exclusive mandatory attribute', 'amp' ); $attributes_key = 'mandatory_oneof_attrs'; } else { $title = esc_html__( 'Missing at least one mandatory attribute', 'amp' ); $attributes_key = 'mandatory_anyof_attrs'; } // @todo This should not be needed because we can look it up from the spec. See https://github.com/ampproject/amp-wp/pull/3817. if ( ! empty( $validation_error[ $attributes_key ] ) ) { $title .= ': '; $title .= implode( ', ', array_map( static function ( $attribute_name ) { return sprintf( '%s', $attribute_name ); }, $validation_error[ $attributes_key ] ) ); } return $title; case AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_CHILD_TAG: return sprintf( /* translators: %1$s is the child tag, %2$s is node name */ esc_html__( 'Tag %1$s is disallowed as child of tag %2$s', 'amp' ), '' . esc_html( $validation_error['child_tag'] ) . '', '' . esc_html( $validation_error['node_name'] ) . '' ); case AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_FIRST_CHILD_TAG: return sprintf( /* translators: %1$s is the first child tag, %2$s is node name */ esc_html__( 'Tag %1$s is disallowed as first child of tag %2$s', 'amp' ), '' . esc_html( $validation_error['first_child_tag'] ) . '', '' . esc_html( $validation_error['node_name'] ) . '' ); case AMP_Tag_And_Attribute_Sanitizer::INCORRECT_NUM_CHILD_TAGS: return sprintf( esc_html( /* translators: %1$s is the node name, %2$s is required child count */ _n( 'Tag %1$s must have %2$s child tag', 'Tag %1$s must have %2$s child tags', (int) $validation_error['required_child_count'], 'amp' ) ), '' . esc_html( $validation_error['node_name'] ) . '', esc_html( number_format_i18n( (int) $validation_error['required_child_count'] ) ) ); case AMP_Tag_And_Attribute_Sanitizer::INCORRECT_MIN_NUM_CHILD_TAGS: return sprintf( esc_html( /* translators: %1$s is the node name, %2$s is required child count */ _n( 'Tag %1$s must have a minimum of %2$s child tag', 'Tag %1$s must have a minimum of %2$s child tags', (int) $validation_error['required_min_child_count'], 'amp' ) ), '' . esc_html( $validation_error['node_name'] ) . '', esc_html( number_format_i18n( (int) $validation_error['required_min_child_count'] ) ) ); case AMP_Tag_And_Attribute_Sanitizer::WRONG_PARENT_TAG: return sprintf( /* translators: %1$s is the node name, %2$s is parent name */ esc_html__( 'The parent tag of tag %1$s cannot be %2$s', 'amp' ), '' . esc_html( $validation_error['node_name'] ) . '', '' . esc_html( $validation_error['parent_name'] ) . '' ); case AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG_ANCESTOR: case AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_DESCENDANT_TAG: return sprintf( /* translators: %1$s is the node name, %2$s is the disallowed ancestor tag name */ esc_html__( 'The tag %1$s may not appear as a descendant of tag %2$s', 'amp' ), '' . esc_html( $validation_error['node_name'] ) . '', '' . esc_html( $validation_error['disallowed_ancestor'] ) . '' ); case AMP_Tag_And_Attribute_Sanitizer::MANDATORY_TAG_ANCESTOR: return sprintf( /* translators: %1$s is the node name, %2$s is the required ancestor tag name */ esc_html__( 'The tag %1$s may only appear as a descendant of tag %2$s', 'amp' ), '' . esc_html( $validation_error['node_name'] ) . '', '' . esc_html( $validation_error['required_ancestor_name'] ) . '' ); case AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE: case AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE_CASEI: case AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE_REGEX: case AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE_REGEX_CASEI: case AMP_Tag_And_Attribute_Sanitizer::INVALID_DISALLOWED_VALUE_REGEX: return sprintf( /* translators: %s is the attribute name */ esc_html__( 'The attribute %s is set to an invalid value', 'amp' ), '' . esc_html( $validation_error['node_name'] ) . '' ); case AMP_Tag_And_Attribute_Sanitizer::INVALID_URL_PROTOCOL: $parsed_url = wp_parse_url( $validation_error['element_attributes'][ $validation_error['node_name'] ] ); $invalid_protocol = isset( $parsed_url['scheme'] ) ? $parsed_url['scheme'] . ':' : '(null)'; return sprintf( /* translators: %1$s is the invalid protocol, %2$s is attribute name */ esc_html__( 'Invalid URL protocol %1$s for attribute %2$s', 'amp' ), '' . esc_html( $invalid_protocol ) . '', '' . esc_html( $validation_error['node_name'] ) . '' ); case AMP_Tag_And_Attribute_Sanitizer::INVALID_URL: return sprintf( /* translators: %1$s is the invalid URL, %2$s is attribute name */ esc_html__( 'Malformed URL %1$s for attribute %2$s', 'amp' ), '' . esc_html( $validation_error['element_attributes'][ $validation_error['node_name'] ] ) . '', '' . esc_html( $validation_error['node_name'] ) . '' ); case AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_RELATIVE_URL: return sprintf( /* translators: %1$s is the relative URL, %2$s is attribute name */ esc_html__( 'The relative URL %1$s for attribute %2$s is disallowed', 'amp' ), '' . esc_html( $validation_error['element_attributes'][ $validation_error['node_name'] ] ) . '', '' . esc_html( $validation_error['node_name'] ) . '' ); case AMP_Tag_And_Attribute_Sanitizer::MISSING_URL: return sprintf( /* translators: %1$s is attribute name */ esc_html__( 'Missing URL for attribute %s', 'amp' ), '' . esc_html( $validation_error['node_name'] ) . '' ); case AMP_Tag_And_Attribute_Sanitizer::DUPLICATE_DIMENSIONS: return sprintf( /* translators: %1$s is the attribute name, %2$s is the tag name */ esc_html__( 'Multiple image candidates with the same width or pixel density found in attribute %1$s in tag %2$s', 'amp' ), '' . esc_html( $validation_error['node_name'] ) . '', '' . esc_html( $validation_error['parent_name'] ) . '' ); case AMP_Tag_And_Attribute_Sanitizer::INVALID_LAYOUT_WIDTH: case AMP_Tag_And_Attribute_Sanitizer::INVALID_LAYOUT_HEIGHT: case AMP_Tag_And_Attribute_Sanitizer::INVALID_LAYOUT_AUTO_HEIGHT: case AMP_Tag_And_Attribute_Sanitizer::INVALID_LAYOUT_NO_HEIGHT: case AMP_Tag_And_Attribute_Sanitizer::INVALID_LAYOUT_FIXED_HEIGHT: case AMP_Tag_And_Attribute_Sanitizer::INVALID_LAYOUT_AUTO_WIDTH: case AMP_Tag_And_Attribute_Sanitizer::INVALID_LAYOUT_HEIGHTS: if ( isset( $validation_error['node_attributes'][ $validation_error['attribute'] ] ) ) { return sprintf( /* translators: %1$s is the invalid attribute value, %2$s is the attribute name */ esc_html__( 'Invalid value %1$s for attribute %2$s', 'amp' ), '' . esc_html( $validation_error['node_attributes'][ $validation_error['attribute'] ] ) . '', '' . esc_html( $validation_error['attribute'] ) . '' ); } else { return sprintf( /* translators: %s is the invalid attribute value */ esc_html__( 'Invalid or missing value for attribute %s', 'amp' ), '' . esc_html( $validation_error['attribute'] ) . '' ); } case AMP_Tag_And_Attribute_Sanitizer::INVALID_LAYOUT_UNIT_DIMENSIONS: return esc_html__( 'Inconsistent units for width and height', 'amp' ); case AMP_Tag_And_Attribute_Sanitizer::MISSING_LAYOUT_ATTRIBUTES: return sprintf( /* translators: %1$s is the element name, %2$s is the attribute name 'width', %3$s is the attribute name 'height' */ esc_html__( 'Incomplete layout attributes specified for tag %1$s. For example, provide attributes %2$s and %3$s', 'amp' ), '' . esc_html( $validation_error['node_name'] ) . '', 'width', 'height' ); case AMP_Tag_And_Attribute_Sanitizer::IMPLIED_LAYOUT_INVALID: return sprintf( /* translators: %1$s is the layout, %2$s is the tag */ esc_html__( 'The implied layout %1$s is not supported by tag %2$s.', 'amp' ), '' . esc_html( $validation_error['layout'] ) . '', '' . esc_html( $validation_error['node_name'] ) . '' ); case AMP_Tag_And_Attribute_Sanitizer::SPECIFIED_LAYOUT_INVALID: return sprintf( /* translators: %1$s is the layout, %2$s is the tag */ esc_html__( 'The specified layout %1$s is not supported by tag %2$s.', 'amp' ), '' . esc_html( $validation_error['layout'] ) . '', '' . esc_html( $validation_error['node_name'] ) . '' ); case AMP_Form_Sanitizer::POST_FORM_HAS_ACTION_XHR_WHEN_NATIVE_USED: return sprintf( /* translators: %1$s is 'POST', %2$s is 'action-xhr' */ esc_html__( 'Native %1$s form has %2$s attribute.', 'amp' ), 'POST', 'action-xhr' ); case AMP_Script_Sanitizer::CUSTOM_EXTERNAL_SCRIPT: return sprintf( /* translators: %s is script basename */ esc_html__( 'Custom external script %s encountered', 'amp' ), '' . basename( strtok( $validation_error['node_attributes']['src'], '?#' ) ) . '' ); case AMP_Script_Sanitizer::CUSTOM_INLINE_SCRIPT: return esc_html__( 'Custom inline script encountered', 'amp' ); case AMP_Script_Sanitizer::CUSTOM_EVENT_HANDLER_ATTR: return sprintf( /* translators: %s is attribute name */ esc_html__( 'Event handler attribute %s encountered', 'amp' ), '' . $validation_error['node_name'] . '' ); default: /* translators: %s error code */ return sprintf( esc_html__( 'Unknown error (%s)', 'amp' ), $validation_error['code'] ); } } /** * Get label for object key in validation error source. * * @param string $key Key. * @param array $validation_error Validation error. * @return string Label for key. */ public static function get_source_key_label( $key, $validation_error ) { switch ( $key ) { case 'code': return __( 'Code', 'amp' ); case 'at_rule': return __( 'At-rule', 'amp' ); case 'node_attributes': case 'element_attributes': return __( 'Element attributes', 'amp' ); case 'node_name': if ( AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR === $validation_error['code'] ) { return __( 'Attribute name', 'amp' ); } elseif ( AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG === $validation_error['code'] ) { return __( 'Element name', 'amp' ); } else { return __( 'Node name', 'amp' ); } case 'parent_name': return __( 'Parent element', 'amp' ); case 'css_property_name': return __( 'CSS property', 'amp' ); case 'css_selector': return __( 'CSS selector', 'amp' ); case 'class_name': return __( 'Class name', 'amp' ); case 'duplicate_oneof_attrs': return __( 'Mutually exclusive attributes', 'amp' ); case 'text': return __( 'Text content', 'amp' ); case 'type': return __( 'Type', 'amp' ); case 'sources': return __( 'Sources', 'amp' ); case 'meta_property_name': if ( AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_PROPERTY_IN_ATTR_VALUE === $validation_error['code'] || AMP_Tag_And_Attribute_Sanitizer::MISSING_REQUIRED_PROPERTY_VALUE === $validation_error['code'] ) { return __( 'Invalid property', 'amp' ); } elseif ( AMP_Tag_And_Attribute_Sanitizer::MISSING_MANDATORY_PROPERTY === $validation_error['code'] ) { return __( 'Missing property', 'amp' ); } return __( 'Property name', 'amp' ); case 'property_value': if ( AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_PROPERTY_IN_ATTR_VALUE === $validation_error['code'] ) { return __( 'Invalid property value', 'amp' ); } elseif ( AMP_Tag_And_Attribute_Sanitizer::MISSING_REQUIRED_PROPERTY_VALUE === $validation_error['code'] ) { return __( 'Required property value', 'amp' ); } return __( 'Property value', 'amp' ); case 'attributes': return __( 'Missing attributes', 'amp' ); case 'child_tag': return __( 'Child tag', 'amp' ); case 'first_child_tag': return __( 'First child tag', 'amp' ); case 'children_count': return __( 'Children count', 'amp' ); case 'required_child_count': return __( 'Required child count', 'amp' ); case 'required_min_child_count': return __( 'Required minimum child count', 'amp' ); case 'required_parent_name': return __( 'Required parent element', 'amp' ); case 'disallowed_ancestor': return __( 'Disallowed ancestor element', 'amp' ); case 'required_ancestor_name': return __( 'Required ancestor element', 'amp' ); case 'attribute': return __( 'Invalid attribute', 'amp' ); case 'required_attr_value': return __( 'Required attribute value', 'amp' ); case 'url': return __( 'URL', 'amp' ); case 'message': return __( 'Message', 'amp' ); case 'duplicate_dimensions': return __( 'Duplicate dimensions', 'amp' ); default: return $key; } } /** * Get Status Text with Icon * * @see \AMP_Validation_Error_Taxonomy::get_validation_error_sanitization() * * @param array $sanitization Sanitization. * @param bool $include_reviewed Include reviewed/unreviewed status. * @return string Status text. */ public static function get_status_text_with_icon( $sanitization, $include_reviewed = false ) { if ( $sanitization['term_status'] & self::ACCEPTED_VALIDATION_ERROR_BIT_MASK ) { $icon = Icon::removed(); $text = __( 'Removed', 'amp' ); } else { $icon = Icon::invalid(); $text = __( 'Kept', 'amp' ); } if ( $include_reviewed ) { $text .= ' ('; if ( $sanitization['term_status'] & self::ACKNOWLEDGED_VALIDATION_ERROR_BIT_MASK ) { $text .= __( 'Reviewed', 'amp' ); } else { $text .= __( 'Unreviewed', 'amp' ); } $text .= ')'; } return sprintf( '%s %s', $icon->to_html(), esc_html( $text ) ); } /** * Deletes cached term counts. */ public static function clear_cached_counts() { delete_transient( self::TRANSIENT_KEY_ERROR_INDEX_COUNTS ); } }