'; esc_html_e( 'Warning: There is content which fails AMP validation; it will be stripped when served as AMP.', 'amp' ); echo sprintf( ' %s', esc_url( get_edit_post_link( $validation_status_post ) ), esc_html__( 'Details', 'amp' ) ); echo ' | '; echo sprintf( ' %s', esc_url( self::get_debug_url( $url ) ), esc_attr__( 'Validate URL on frontend but without invalid elements/attributes removed', 'amp' ), esc_html__( 'Debug', 'amp' ) ); echo '
'; $results = self::summarize_validation_errors( array_unique( $validation_errors, SORT_REGULAR ) ); $removed_sets = array(); if ( ! empty( $results[ self::REMOVED_ELEMENTS ] ) && is_array( $results[ self::REMOVED_ELEMENTS ] ) ) { $removed_sets[] = array( 'label' => __( 'Invalid elements:', 'amp' ), 'names' => array_map( 'sanitize_key', $results[ self::REMOVED_ELEMENTS ] ), ); } if ( ! empty( $results[ self::REMOVED_ATTRIBUTES ] ) && is_array( $results[ self::REMOVED_ATTRIBUTES ] ) ) { $removed_sets[] = array( 'label' => __( 'Invalid attributes:', 'amp' ), 'names' => array_map( 'sanitize_key', $results[ self::REMOVED_ATTRIBUTES ] ), ); } // @todo There are other kinds of errors other than REMOVED_ELEMENTS and REMOVED_ATTRIBUTES. foreach ( $removed_sets as $removed_set ) { printf( '%s ', esc_html( $removed_set['label'] ) ); self::output_removed_set( $removed_set['names'] ); echo '
'; } echo '.
*
* @param array[][] $set {
* The removed elements to output.
*
* @type string $name The name of the source.
* @type string $count The number that were invalid.
* }
* @return void
*/
protected static function output_removed_set( $set ) {
$items = array();
foreach ( $set as $name => $count ) {
if ( 1 === intval( $count ) ) {
$items[] = sprintf( '%s', esc_html( $name ) );
} else {
$items[] = sprintf( '%s (%d)', esc_html( $name ), $count );
}
}
echo implode( ', ', $items ); // WPCS: XSS OK.
}
/**
* Whether to validate the front end response.
*
* Either the user has the capability and the query var is present.
*
* @return boolean Whether to validate.
*/
public static function should_validate_response() {
return self::has_cap() && isset( $_GET[ self::VALIDATE_QUERY_VAR ] ); // WPCS: CSRF ok.
}
/**
* Finalize validation.
*
* @param DOMDocument $dom Document.
* @param array $args {
* Args.
*
* @type bool $remove_source_comments Whether source comments should be removed. Defaults to true.
* @type bool $append_validation_status_comment Whether the validation errors should be appended as an HTML comment. Defaults to true.
* }
*/
public static function finalize_validation( DOMDocument $dom, $args = array() ) {
$args = array_merge(
array(
'remove_source_comments' => true,
'append_validation_status_comment' => true,
),
$args
);
if ( $args['remove_source_comments'] ) {
self::remove_source_comments( $dom );
}
if ( $args['append_validation_status_comment'] ) {
$encoded = wp_json_encode( self::$validation_errors, 128 /* JSON_PRETTY_PRINT */ );
$encoded = str_replace( '--', '\u002d\u002d', $encoded ); // Prevent "--" in strings from breaking out of HTML comments.
$comment = $dom->createComment( 'AMP_VALIDATION_ERRORS:' . $encoded . "\n" );
$dom->documentElement->appendChild( $comment );
}
}
/**
* Adds the validation callback if front-end validation is needed.
*
* @param array $sanitizers The AMP sanitizers.
* @return array $sanitizers The filtered AMP sanitizers.
*/
public static function add_validation_callback( $sanitizers ) {
foreach ( $sanitizers as $sanitizer => $args ) {
$sanitizers[ $sanitizer ] = array_merge(
$args,
array(
'validation_error_callback' => __CLASS__ . '::add_validation_error',
)
);
}
return $sanitizers;
}
/**
* Registers the post type to store the validation errors.
*
* @return void.
*/
public static function register_post_type() {
$post_type = register_post_type(
self::POST_TYPE_SLUG,
array(
'labels' => array(
'name' => _x( 'Validation Status', 'post type general name', 'amp' ),
'singular_name' => __( 'validation error', 'amp' ),
'not_found' => __( 'No validation errors found', 'amp' ),
'not_found_in_trash' => __( 'No validation errors found in trash', 'amp' ),
'search_items' => __( 'Search statuses', 'amp' ),
'edit_item' => __( 'Validation Status', 'amp' ),
),
'supports' => false,
'public' => false,
'show_ui' => true,
'show_in_menu' => AMP_Options_Manager::OPTION_NAME,
)
);
// Hide the add new post link.
$post_type->cap->create_posts = 'do_not_allow';
}
/**
* Stores the validation errors.
*
* After the preprocessors run, this gets the validation response if the query var is present.
* It then stores the response in a custom post type.
* If there's already an error post for the URL, but there's no error anymore, it deletes it.
*
* @param array $validation_errors Validation errors.
* @param string $url URL on which the validation errors occurred.
* @return int|null $post_id The post ID of the custom post type used, or null.
* @global WP $wp
*/
public static function store_validation_errors( $validation_errors, $url ) {
$post_for_this_url = self::get_validation_status_post( $url );
// Since there are no validation errors and there is an existing $existing_post_id, just delete the post.
if ( empty( $validation_errors ) ) {
if ( $post_for_this_url ) {
wp_delete_post( $post_for_this_url->ID, true );
}
return null;
}
$encoded_errors = wp_json_encode( $validation_errors );
$post_name = md5( $encoded_errors );
// If the post name is unchanged then the errors are the same and there is nothing to do.
if ( $post_for_this_url && $post_for_this_url->post_name === $post_name ) {
return $post_for_this_url->ID;
}
// If there already exists a post for the given validation errors, just amend the $url to the existing post.
$post_for_other_url = get_page_by_path( $post_name, OBJECT, self::POST_TYPE_SLUG );
if ( ! $post_for_other_url ) {
$post_for_other_url = get_page_by_path( $post_name . '__trashed', OBJECT, self::POST_TYPE_SLUG );
}
if ( $post_for_other_url ) {
if ( 'trash' === $post_for_other_url->post_status ) {
wp_untrash_post( $post_for_other_url->ID );
}
if ( ! in_array( $url, get_post_meta( $post_for_other_url->ID, self::AMP_URL_META, false ), true ) ) {
add_post_meta( $post_for_other_url->ID, self::AMP_URL_META, wp_slash( $url ), false );
}
return $post_for_other_url->ID;
}
// Otherwise, create a new validation status post, or update the existing one.
$post_id = wp_insert_post( wp_slash( array(
'ID' => $post_for_this_url ? $post_for_this_url->ID : null,
'post_type' => self::POST_TYPE_SLUG,
'post_title' => $url,
'post_name' => $post_name,
'post_content' => $encoded_errors,
'post_status' => 'publish',
) ) );
if ( ! $post_id ) {
return null;
}
if ( ! in_array( $url, get_post_meta( $post_id, self::AMP_URL_META, false ), true ) ) {
add_post_meta( $post_id, self::AMP_URL_META, wp_slash( $url ), false );
}
return $post_id;
}
/**
* Gets the existing custom post that stores errors for the $url, if it exists.
*
* @param string $url The URL of the post.
* @return WP_Post|null The post of the existing custom post, or null.
*/
public static function get_validation_status_post( $url ) {
if ( ! post_type_exists( self::POST_TYPE_SLUG ) ) {
return null;
}
$query = new WP_Query( array(
'post_type' => self::POST_TYPE_SLUG,
'post_status' => 'publish',
'posts_per_page' => 1,
'meta_query' => array(
array(
'key' => self::AMP_URL_META,
'value' => $url,
),
),
) );
return array_shift( $query->posts );
}
/**
* Validates the latest published post.
*
* @return array|WP_Error The validation errors, or WP_Error.
*/
public static function validate_after_plugin_activation() {
$url = amp_admin_get_preview_permalink();
if ( ! $url ) {
return new WP_Error( 'no_published_post_url_available' );
}
$validation_errors = self::validate_url( $url );
if ( is_array( $validation_errors ) && count( $validation_errors ) > 0 ) {
self::store_validation_errors( $validation_errors, $url );
set_transient( self::PLUGIN_ACTIVATION_VALIDATION_ERRORS_TRANSIENT_KEY, $validation_errors, 60 );
} else {
delete_transient( self::PLUGIN_ACTIVATION_VALIDATION_ERRORS_TRANSIENT_KEY );
}
return $validation_errors;
}
/**
* Validates a given URL.
*
* The validation errors will be stored in the validation status custom post type,
* as well as in a transient.
*
* @param string $url The URL to validate.
* @return array|WP_Error The validation errors, or WP_Error on error.
*/
public static function validate_url( $url ) {
$validation_url = add_query_arg(
array(
self::VALIDATE_QUERY_VAR => 1,
self::CACHE_BUST_QUERY_VAR => wp_rand(),
),
$url
);
$r = wp_remote_get( $validation_url, array(
'cookies' => wp_unslash( $_COOKIE ),
'sslverify' => false,
'headers' => array(
'Cache-Control' => 'no-cache',
),
) );
if ( is_wp_error( $r ) ) {
return $r;
}
if ( wp_remote_retrieve_response_code( $r ) >= 400 ) {
return new WP_Error(
wp_remote_retrieve_response_code( $r ),
wp_remote_retrieve_response_message( $r )
);
}
$response = wp_remote_retrieve_body( $r );
if ( ! preg_match( '#