Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Issue #843: Validate Gutenberg blocks for AMP compliance #902

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions amp.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ function amp_after_setup_theme() {

add_action( 'init', 'amp_init' );
add_action( 'widgets_init', 'AMP_Theme_Support::register_widgets' );
add_action( 'admin_enqueue_scripts', 'AMP_Theme_Support::enqueue_editor' );
add_action( 'admin_init', 'AMP_Options_Manager::register_settings' );
add_filter( 'amp_post_template_analytics', 'amp_add_custom_analytics' );
add_action( 'wp_loaded', 'amp_post_meta_box' );
Expand Down
160 changes: 160 additions & 0 deletions assets/js/amp-editor-validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*jshint esversion: 6 */
/*global amp, wp:true */
/**
* AMP Gutenberg integration.
*
* On editing a block, this checks that the content is AMP-compatible.
* And it displays a notice if it's not.
*/

/* exported ampGutenberg */
var ampEditorValidation = ( function( $ ) {
'use strict';

var component = {
/**
* Holds data.
*/
data: {},

/**
* The response from the amp.validator for valid AMP.
*/
validStatus: 'PASS',

/**
* Boot module.
*
* @param {Object} data Object data.
* @return {void}
*/
boot: function( data ) {
component.data = data;
$( document ).ready( function() {
component.getScript();
} );
},

/**
* Gets the amp CDN validator.js script, and run this file.
*
* The getScript callback is a workaround, and not recommended for production.
* It replaces the wp value that the script overwrote.
*
* @returns void.
*/
getScript: function() {
$.getScript( 'https://cdn.ampproject.org/v0/validator.js', function() {
component.validatePage();
if ( 'undefined' !== typeof wp.blocks ) {
component.processBlocks();
}
} );
},

/**
* Gets all of the registered blocks, and overwrites their edit() functions.
*
* The new edit() functions will check if the content is AMP-compliant.
* If not, the block will display a notice.
*
* @returns {void}
*/
processBlocks: function() {
var blocks = wp.blocks.getBlockTypes(),
key = 'name';
blocks.forEach( function( block ) {
if ( block.hasOwnProperty( key ) ) {
component.overwriteEdit( block[ key ] );
}
} );
},

/**
* Overwrites the edit() function of a block.
*
* Retain the original edit function in OriginalBlockEdit.
* If the block's content isn't valid AMP,
* Prepend a notice to the block.
*
* @see https://riad.blog/2017/10/16/one-thousand-and-one-way-to-extend-gutenberg-today/
* @param {string} blockType the type of the block, like 'core/paragraph'.
* @returns {void}
*/
overwriteEdit: function( blockType ) {
var el = wp.element.createElement,
Notice = wp.components.Notice,
block = wp.blocks.unregisterBlockType( blockType ),
OriginalBlockEdit = block.edit;

block.edit = function( props ) {
var result = [ el( OriginalBlockEdit, props ) ],
content = block.save( props );

if ( 'string' !== typeof content ) {
content = wp.element.renderToString( content );
}

// If validation fails, prepend a Notice to the block.
if ( ! component.isValidAMP( content, true ) ) {
result.unshift( el(
Notice,
{
status: 'warning',
content: component.data.i18n.notice,
isDismissible: false
}
) );
}
return result;
};
wp.blocks.registerBlockType( blockType, block );
},

/**
* Whether markup is valid AMP.
*
* Uses the AMP validator from the AMP CDN.
* And places the passed markup inside the <body> tag of a basic valid AMP page.
* Then, validates that page.
*
* @param {string} markup The markup to test.
* @param {boolean} doWrap Whether to wrap the passed markup in a basic AMP document.
* @returns {boolean} $valid Whether the passed markup is valid AMP.
*/
isValidAMP: function( markup, doWrap = false ) {
var validated,
validKey = 'status';
if ( true === doWrap ) {
markup = `<!doctype html><html ⚡><head><meta charset="utf-8"><link rel="canonical" href="./regular-html-version.html"><meta name="viewport" content="width=device-width,minimum-scale=1"><style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript><script async src="https://cdn.ampproject.org/v0.js"></script></head><body>${markup}</body></html>`;
}
validated = amp.validator.validateString( markup );
return ( validated.hasOwnProperty( validKey ) && component.validStatus === validated[ validKey ] );
},

/**
* Validate the entire page that a URL produces.
*
* @returns {void}
*/
validatePage: function() {
if ( ! component.data.hasOwnProperty( 'doValidatePage' ) || true !== component.data.doValidatePage ) {
return;
}

$.get( component.data.permalink, function( data ) {
if ( 'string' === typeof data && ! component.isValidAMP( data ) ) {
let $notice = $( '<div>' )
.addClass( 'notice notice-warning is-dismissible' )
.append( $( '<p>' )
.text( component.data.i18n.notice )
);
$( '.wp-header-end' ).after( $notice );
}
} );
}
};

return component;

} )( window.jQuery );
57 changes: 57 additions & 0 deletions includes/class-amp-theme-support.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,63 @@ public static function override_wp_styles() {
return $wp_styles;
}

/**
* Enqueues the script on post.php.
*
* This enables AMP validation in the editor.
* Both in Gutenberg and the 'Classic' editor.
*
* @return void.
*/
public static function enqueue_editor() {
global $pagenow;
$slug = 'amp-editor-validation';

if ( 'post.php' !== $pagenow ) {
return;
}

wp_enqueue_script(
$slug,
amp_get_asset_url( 'js/amp-editor-validation.js' ),
array( 'jquery' ),
AMP__VERSION,
true
);

$data = wp_json_encode( array(
'i18n' => array(
'notice' => __( 'This is not valid AMP', 'amp' ),
),
'permalink' => esc_url( get_the_permalink() ),
'doValidatePage' => self::do_validate_page(),
) );
wp_add_inline_script( $slug, sprintf( 'ampEditorValidation.boot( %s );', $data ) );
}

/**
* Whether to check the permalink of a post for AMP compliance.
*
* On saving a post in post.php, there should be a message if the entire document isn't valid AMP.
* The request for post.php usually has a value for 'message.'
* This number corresponds to the change in the post.
* For example, 1 applies when the post is updated.
* There are some 'messages' that this should not apply to, like 8 (Post submitted).
* This is called on enqueuing a script for post.php.
* There is no nonce for the page, so this has no nonce verification.
*
* @see $messages in edit-form-advanced.php
* @return boolean $do_validate Whether to validate the post.
*/
public static function do_validate_page() {
$accepted = array( 1, 4, 7, 10 );
if ( ! isset( $_GET['message'] ) ) { // WPCS: CSRF ok.
return false;
}
$message = intval( wp_unslash( $_GET['message'] ) ); // WPCS: CSRF ok.
return in_array( $message, $accepted, true );
}

/**
* Register content embed handlers.
*
Expand Down
22 changes: 22 additions & 0 deletions tests/test-class-amp-theme-support.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,26 @@ public function test_finish_output_buffering() {

$this->assertContains( '<script async custom-element="amp-ad"', $sanitized_html );
}

/**
* Test enqueue_editor.
*
* @covers AMP_Theme_Support::enqueue_editor()
*/
public function test_enqueue_editor() {
global $post;
$post = $this->factory()->post->create();
AMP_Theme_Support::enqueue_editor();
$slug = 'amp-editor-validation';
$script = wp_scripts()->registered[ $slug ];
$this->assertContains( 'js/amp-editor-validation.js', $script->src );
$this->assertEquals( array( 'jquery' ), $script->deps );
$this->assertEquals( AMP__VERSION, $script->ver );
$this->assertTrue( in_array( $slug, wp_scripts()->queue, true ) );
$this->assertContains( 'ampGutenberg.boot', $script->extra['after'][1] );
$this->assertContains( 'This is not valid AMP', $script->extra['after'][1] );
$this->assertContains( wp_json_encode( get_the_permalink() ), $script->extra['after'][1] );
$this->assertContains( 'doValidatePage', $script->extra['after'][1] );
}

}