diff --git a/.eslintrc b/.eslintrc
index 9ca57237473..2ec892c61af 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -57,7 +57,7 @@
"react/no-unused-prop-types": "error",
"react/self-closing-comp": "error",
"import/no-unresolved": [ "error", {
- "ignore": [ "jquery", "amp-block-editor-data", "amp-settings" ]
+ "ignore": [ "jquery", "amp-block-editor-data", "amp-settings", "amp-block-validation" ]
} ],
"import/order": [ "error", { "groups": [ "builtin", [ "external", "unknown" ], "internal", "parent", "sibling", "index" ] } ],
"jsdoc/check-indentation": "error",
diff --git a/.gitattributes b/.gitattributes
index 75e82ba8417..289761dafa2 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -6,3 +6,4 @@
includes/sanitizers/class-amp-allowed-tags-generated.php linguist-generated=true
docs/**/*.md linguist-generated=true
docs/docs.json linguist-generated=true
+**/__data__/*.js linguist-generated=true
diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php
index 804117ba0e1..ebbca0b6c9f 100644
--- a/.phpstorm.meta.php
+++ b/.phpstorm.meta.php
@@ -16,11 +16,13 @@
'amp_slug_customization_watcher' => \AmpProject\AmpWP\AmpSlugCustomizationWatcher::class,
'css_transient_cache.ajax_handler' => \AmpProject\AmpWP\Admin\ReenableCssTransientCachingAjaxAction::class,
'css_transient_cache.monitor' => \AmpProject\AmpWP\BackgroundTask\MonitorCssTransientCaching::class,
+ 'dev_tools.block_sources' => \AmpProject\AmpWP\DevTools\BlockSources::class,
'dev_tools.callback_reflection' => \AmpProject\AmpWP\DevTools\CallbackReflection::class,
'dev_tools.error_page' => \AmpProject\AmpWP\DevTools\ErrorPage::class,
'dev_tools.file_reflection' => \AmpProject\AmpWP\DevTools\FileReflection::class,
'dev_tools.likely_culprit_detector' => \AmpProject\AmpWP\DevTools\LikelyCulpritDetector::class,
'dev_tools.user_access' => \AmpProject\AmpWP\DevTools\UserAccess::class,
+ 'editor.editor_support' => \AmpProject\AmpWP\Editor\EditorSupport::class,
'extra_theme_and_plugin_headers' => \AmpProject\AmpWP\ExtraThemeAndPluginHeaders::class,
'injector' => \AmpProject\AmpWP\Infrastructure\Injector::class,
'mobile_redirection' => \AmpProject\AmpWP\MobileRedirection::class,
diff --git a/assets/css/src/amp-validation-single-error-url.css b/assets/css/src/amp-validation-single-error-url.css
index 3e3d7a4e8b2..5f029facfa7 100644
--- a/assets/css/src/amp-validation-single-error-url.css
+++ b/assets/css/src/amp-validation-single-error-url.css
@@ -1,3 +1,23 @@
+#the-list tr {
+ scroll-margin-top: 32px;
+}
+
+@media screen and (max-width: 782px) {
+
+ #the-list tr {
+ scroll-margin-top: 46px;
+ }
+}
+
+@media screen and (max-width: 600px) {
+
+ #the-list tr {
+
+ /* Since the admin bar is not sticky */
+ scroll-margin-top: 0;
+ }
+}
+
/** Arrow icon on title in error column. */
.column-error_code > .single-url-detail-toggle {
position: relative;
diff --git a/assets/images/amp-alert.svg b/assets/images/amp-alert.svg
new file mode 100644
index 00000000000..24acfb27d90
--- /dev/null
+++ b/assets/images/amp-alert.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/amp-css-error-icon.svg b/assets/images/amp-css-error-icon.svg
new file mode 100644
index 00000000000..f2a17179b0f
--- /dev/null
+++ b/assets/images/amp-css-error-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/amp-delete.svg b/assets/images/amp-delete.svg
new file mode 100644
index 00000000000..444cf74ec1d
--- /dev/null
+++ b/assets/images/amp-delete.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/amp-html-error-icon.svg b/assets/images/amp-html-error-icon.svg
new file mode 100644
index 00000000000..3f78c6aa80c
--- /dev/null
+++ b/assets/images/amp-html-error-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/amp-icon-toolbar.svg b/assets/images/amp-icon-toolbar.svg
new file mode 100644
index 00000000000..c3681743c55
--- /dev/null
+++ b/assets/images/amp-icon-toolbar.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/amp-js-error-icon.svg b/assets/images/amp-js-error-icon.svg
new file mode 100644
index 00000000000..db6b8c5807f
--- /dev/null
+++ b/assets/images/amp-js-error-icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/images/amp-toolbar-icon-broken.svg b/assets/images/amp-toolbar-icon-broken.svg
new file mode 100644
index 00000000000..c60ae4a3c72
--- /dev/null
+++ b/assets/images/amp-toolbar-icon-broken.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/images/amp-validation-errors-kept.svg b/assets/images/amp-validation-errors-kept.svg
new file mode 100644
index 00000000000..9b13d0f17ee
--- /dev/null
+++ b/assets/images/amp-validation-errors-kept.svg
@@ -0,0 +1,14 @@
+
diff --git a/assets/images/amp-validation-errors.svg b/assets/images/amp-validation-errors.svg
new file mode 100644
index 00000000000..a374fe793a3
--- /dev/null
+++ b/assets/images/amp-validation-errors.svg
@@ -0,0 +1,23 @@
+
diff --git a/assets/src/amp-validation/amp-validation-single-error-url-details.js b/assets/src/amp-validation/amp-validation-single-error-url-details.js
index 110b5195605..32e9d0d6994 100644
--- a/assets/src/amp-validation/amp-validation-single-error-url-details.js
+++ b/assets/src/amp-validation/amp-validation-single-error-url-details.js
@@ -7,9 +7,10 @@ import domReady from '@wordpress/dom-ready';
* Toggles the contents of a details element as an additional table tr.
*/
class RowToggler {
- constructor( tr, index ) {
+ constructor( tr, index, activeTermId ) {
this.tr = tr;
this.index = index;
+ this.activeTermId = activeTermId;
// Since we're adding additional rows, we need to override default .striped tables styles.
this.tr.classList.add( this.index % 2 ? 'odd' : 'even' );
@@ -33,6 +34,19 @@ class RowToggler {
} );
} );
}
+
+ this.maybeInitiallyOpenRow();
+ }
+
+ /**
+ * If the term ID retrieved from the URL query param matches this row's term ID, expand the row on load.
+ */
+ maybeInitiallyOpenRow() {
+ if ( ! this.activeTermId || this.tr.id !== `tag-${ this.activeTermId }` ) {
+ return;
+ }
+
+ this.toggle( this.tr.querySelector( '.single-url-detail-toggle' ) );
}
/**
@@ -71,7 +85,7 @@ class RowToggler {
*
* @param {Object} target The click event target.
*/
- toggle = ( target ) => {
+ toggle( target ) {
if ( this.tr.classList.contains( 'expanded' ) ) {
this.onClose( target );
} else {
@@ -112,10 +126,10 @@ class RowToggler {
* Sets up expandable details for errors when viewing a single URL error list.
*/
class ErrorRows {
- constructor() {
+ constructor( activeTermId ) {
this.rows = [ ...document.querySelectorAll( '.wp-list-table tr[id^="tag-"]' ) ]
.map( ( tr, index ) => {
- const rowHandler = new RowToggler( tr, index );
+ const rowHandler = new RowToggler( tr, index, activeTermId );
rowHandler.init();
return rowHandler;
} )
@@ -153,5 +167,12 @@ class ErrorRows {
}
domReady( () => {
- new ErrorRows().init();
+ let activeTermId = null;
+
+ const matches = window.location.hash.match( /^#tag-(\d+)/ );
+ if ( matches ) {
+ activeTermId = parseInt( matches[ 1 ] );
+ }
+
+ new ErrorRows( activeTermId ).init();
} );
diff --git a/assets/src/block-validation/__mocks__/amp-block-validation.js b/assets/src/block-validation/__mocks__/amp-block-validation.js
new file mode 100644
index 00000000000..90dc2081427
--- /dev/null
+++ b/assets/src/block-validation/__mocks__/amp-block-validation.js
@@ -0,0 +1,40 @@
+module.exports = {
+ blockSources: {
+ 'my-plugin/test-block': {
+ source: 'plugin',
+ title: 'My plugin',
+ },
+ 'my-mu-plugin/test-block': {
+ source: 'mu-plugin',
+ title: 'My MU plugin',
+ },
+ 'my-theme/test-block': {
+ source: 'theme',
+ title: 'My theme',
+ },
+ 'core/test-block': {
+ source: '',
+ title: 'WordPress core',
+ },
+ 'unknown/test-block': {
+ source: '',
+ name: '',
+ },
+ },
+ CSS_ERROR_TYPE: 'css_error',
+ HTML_ATTRIBUTE_ERROR_TYPE: 'html_attribute_error',
+ HTML_ELEMENT_ERROR_TYPE: 'html_element_error',
+ JS_ERROR_TYPE: 'js_error',
+ pluginNames: {
+ 'test-plugin': 'Test plugin',
+ 'test-mu-plugin': 'Test MU plugin',
+ 'test-plugin-2': 'Test plugin 2',
+ 'test-mu-plugin-2': 'Test MU plugin 2',
+ },
+ themeName: 'Test theme',
+ themeSlug: 'test-theme',
+ VALIDATION_ERROR_NEW_REJECTED_STATUS: 0,
+ VALIDATION_ERROR_NEW_ACCEPTED_STATUS: 1,
+ VALIDATION_ERROR_ACK_REJECTED_STATUS: 2,
+ VALIDATION_ERROR_ACK_ACCEPTED_STATUS: 3,
+};
diff --git a/assets/src/block-validation/amp-toolbar-button.js b/assets/src/block-validation/amp-toolbar-button.js
new file mode 100644
index 00000000000..1ff6495ff37
--- /dev/null
+++ b/assets/src/block-validation/amp-toolbar-button.js
@@ -0,0 +1,63 @@
+/**
+ * External dependencies
+ */
+import PropTypes from 'prop-types';
+
+/**
+ * WordPress dependencies
+ */
+import { BlockControls } from '@wordpress/block-editor';
+import { ToolbarButton } from '@wordpress/components';
+import { useDispatch } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { ToolbarIcon } from './icon';
+import { PLUGIN_NAME, SIDEBAR_NAME } from '.';
+
+/**
+ * AMP button displaying in the block toolbar.
+ *
+ * @param {Object} props Component props.
+ * @param {string} props.clientId Block Client ID.
+ * @param {number} props.count The number of errors associated with the block.
+ */
+export function AMPToolbarButton( { clientId, count } ) {
+ const { openGeneralSidebar } = useDispatch( 'core/edit-post' );
+
+ return (
+
- { sprintf(
- /* translators: %s is the number of issues */
- _n(
- 'There is %s issue from AMP validation.',
- 'There are %s issues from AMP validation.',
- errorCount,
- 'amp',
- ),
- errorCount,
- ) }
-
-
- { blockValidationErrors.map( ( error, key ) => {
- return (
-
-
- { code || __( 'unknown', 'amp' ) }
-
- >
- );
-};
-
-ValidationErrorMessage.propTypes = {
- message: PropTypes.string,
- title: PropTypes.string,
- code: PropTypes.string,
-};
-
-export default ValidationErrorMessage;
diff --git a/assets/src/block-validation/components/validation-error-message/test/__snapshots__/index.js.snap b/assets/src/block-validation/components/validation-error-message/test/__snapshots__/index.js.snap
deleted file mode 100644
index b863e11570b..00000000000
--- a/assets/src/block-validation/components/validation-error-message/test/__snapshots__/index.js.snap
+++ /dev/null
@@ -1,30 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ValidationErrorMessage renders an error for a custom error code 1`] = `
-Array [
- "Error code: ",
-
- some_other_error
-
,
-]
-`;
-
-exports[`ValidationErrorMessage renders an error with a title 1`] = `
-
- Invalid attribute
-
- onclick
-
-
-`;
-
-exports[`ValidationErrorMessage renders an error for an unknown error code 1`] = `
-Array [
- "Error code: ",
-
- unknown
-
,
-]
-`;
-
-exports[`ValidationErrorMessage renders an error with a custom message 1`] = `null`;
diff --git a/assets/src/block-validation/components/validation-error-message/test/index.js b/assets/src/block-validation/components/validation-error-message/test/index.js
deleted file mode 100644
index db90e62fbba..00000000000
--- a/assets/src/block-validation/components/validation-error-message/test/index.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * External dependencies
- */
-import { render } from 'enzyme';
-
-/**
- * Internal dependencies
- */
-import ValidationErrorMessage from '../';
-
-describe( 'ValidationErrorMessage', () => {
- it( 'renders an error with a custom message', () => {
- const errorMessage = render(
+ { __( 'This error comes from outside the post content.', 'amp' ) } +
+ ) } +jquery.js
',
+ error: {
+ type: 'js_error',
+ sources: [],
+ },
+ },
+ {
+ clientId: themeBlock.clientId,
+ code: 'DISALLOWED_TAG',
+ status: 3,
+ term_id: 12,
+ title: 'Invalid script: jquery.js
',
+ error: {
+ type: 'js_error',
+ },
+ },
+ {
+ clientId: coreBlock.clientId,
+ code: 'DISALLOWED_TAG',
+ status: 3,
+ term_id: 12,
+ title: 'Invalid script: jquery.js
',
+ error: {
+ type: 'js_error',
+ sources: [],
+ },
+ },
+ {
+ clientId: unknownBlock.clientId,
+ code: 'DISALLOWED_TAG',
+ status: 3,
+ term_id: 12,
+ title: 'Invalid script: jquery.js
',
+ error: {
+ type: 'js_error',
+ sources: [],
+ },
+ },
+ ],
+ } );
+}
+
+function getTestBlock( type ) {
+ switch ( type ) {
+ case 'plugin':
+ return pluginBlock;
+
+ case 'theme':
+ return themeBlock;
+
+ case 'core':
+ return coreBlock;
+
+ case 'unknown':
+ return unknownBlock;
+
+ default:
+ return null;
+ }
+}
+
+describe( 'Error', () => {
+ beforeAll( () => {
+ createTestStoreAndBlocks();
+ } );
+
+ beforeEach( () => {
+ container = document.createElement( 'ul' );
+ document.body.appendChild( container );
+ } );
+
+ afterEach( () => {
+ document.body.removeChild( container );
+ container = null;
+ } );
+
+ it.each( [
+ VALIDATION_ERROR_ACK_ACCEPTED_STATUS,
+ VALIDATION_ERROR_ACK_REJECTED_STATUS,
+ VALIDATION_ERROR_NEW_ACCEPTED_STATUS,
+ VALIDATION_ERROR_NEW_REJECTED_STATUS,
+ ].map( ( status ) => [
+ status,
+ () => (
+
+ { reviewLink && (
+
+ { __( 'Validation issues will be checked for when the post is saved.', 'amp' ) } +
++ { __( 'There are no AMP validation issues.', 'amp' ) } +
++ { __( 'All AMP validation issues have been reviewed.', 'amp' ) } +
+jquery.js
',
+ error: {
+ node_name: 'script',
+ parent_name: 'head',
+ code: 'DISALLOWED_TAG',
+ type: 'js_error',
+ node_attributes: {
+ src: 'https://test.site/wp-includes/js/jquery/jquery.js?ver=__normalized__',
+ id: 'jquery-core-js',
+ },
+ node_type: 1,
+ sources: [],
+ },
+ status: 3,
+ term_status: 3,
+ forced: false,
+ term_id: 12,
+ },
+ {
+ sanitized: false,
+ title: 'Invalid attribute: bad-attr
',
+ error: {
+ code: 'DISALLOWED_ATTR',
+ element_attributes: {
+ 'bad-attr': 'bad-attr',
+ },
+ node_name: 'bad-attr',
+ parent_name: 'div',
+ type: 'html_attribute_error',
+ node_type: 2,
+ sources: [
+ {
+ hook: 'the_content',
+ filter: true,
+ post_id: 1,
+ post_type: 'post',
+ sources: [],
+ },
+ {
+ block_name: 'core/html',
+ post_id: 1,
+ block_content_index: 1,
+ },
+ ],
+ },
+ status: 1,
+ term_status: 1,
+ forced: false,
+ term_id: 27,
+ },
+ {
+ sanitized: false,
+ title: 'Invalid element: <bad-element>
',
+ error: {
+ node_name: 'bad-element',
+ parent_name: 'div',
+ code: 'DISALLOWED_TAG',
+ type: 'html_element_error',
+ node_attributes: [],
+ node_type: 1,
+ sources: [
+ {
+ hook: 'the_content',
+ filter: true,
+ post_id: 1,
+ post_type: 'post',
+ sources: [],
+ },
+ {
+ block_name: 'core/html',
+ post_id: 1,
+ block_content_index: 2,
+ },
+ ],
+ },
+ status: 1,
+ term_status: 1,
+ forced: false,
+ term_id: 28,
+ },
+ {
+ sanitized: false,
+ title: 'Invalid inline script',
+ error: {
+ node_name: 'script',
+ parent_name: 'p',
+ code: 'DISALLOWED_TAG',
+ type: 'js_error',
+ node_attributes: [],
+ text: "alert('danger');",
+ node_type: 1,
+ sources: [
+ {
+ hook: 'the_content',
+ filter: true,
+ post_id: 1,
+ post_type: 'post',
+ sources: [],
+ },
+ {
+ block_name: 'core/html',
+ post_id: 1,
+ block_content_index: 3,
+ },
+ ],
+ },
+ status: 1,
+ term_status: 1,
+ forced: false,
+ term_id: 29,
+ },
+ {
+ sanitized: false,
+ title: 'Invalid inline script',
+ error: {
+ node_name: 'script',
+ parent_name: 'div',
+ code: 'DISALLOWED_TAG',
+ type: 'js_error',
+ node_attributes: {
+ class: 'wp-block-bad-blocks-script',
+ },
+ text: 'alert("hello")',
+ node_type: 1,
+ sources: [
+ {
+ hook: 'the_content',
+ filter: true,
+ post_id: 1,
+ post_type: 'post',
+ sources: [],
+ },
+ {
+ block_name: 'bad-blocks/script',
+ post_id: 1,
+ block_content_index: 10,
+ },
+ ],
+ },
+ status: 1,
+ term_status: 1,
+ forced: false,
+ term_id: 33,
+ },
+ {
+ sanitized: false,
+ title: 'Invalid element: <unknown-element>
',
+ error: {
+ node_name: 'unknown-element',
+ parent_name: 'div',
+ code: 'DISALLOWED_TAG',
+ type: 'html_element_error',
+ node_attributes: {
+ class: 'wp-block-bad-blocks-unknown-element',
+ },
+ node_type: 1,
+ sources: [
+ {
+ hook: 'the_content',
+ filter: true,
+ post_id: 1,
+ post_type: 'post',
+ sources: [],
+ },
+ {
+ block_name: 'bad-blocks/unknown-element',
+ post_id: 1,
+ block_content_index: 11,
+ },
+ ],
+ },
+ status: 1,
+ term_status: 1,
+ forced: false,
+ term_id: 34,
+ },
+ {
+ sanitized: false,
+ title: 'Invalid inline script',
+ error: {
+ node_name: 'script',
+ parent_name: 'body',
+ code: 'DISALLOWED_TAG',
+ type: 'js_error',
+ node_attributes: [],
+ text: "\n\t\t( function ( body ) {\n\t\t\t'use strict';\n\t\t\tbody.className = body.className.replace( /\\bTest-no-js\\b/, 'Test-js' );\n\t\t} )( document.body );\n\t\t",
+ node_type: 1,
+ sources: [
+ {
+ type: 'plugin',
+ name: 'the-test-calendar',
+ file: 'common/src/Test/Main.php',
+ line: 401,
+ function: 'Test__Main::toggle_js_class',
+ hook: 'wp_footer',
+ priority: 10,
+ },
+ ],
+ },
+ status: 2,
+ term_status: 2,
+ forced: false,
+ term_id: 10,
+ },
+ {
+ sanitized: true,
+ title: 'Invalid inline script',
+ error: {
+ node_name: 'script',
+ parent_name: 'body',
+ code: 'DISALLOWED_TAG',
+ type: 'js_error',
+ node_attributes: [],
+ text: ' /* */ ',
+ node_type: 1,
+ sources: [
+ {
+ type: 'plugin',
+ name: 'the-test-calendar',
+ file: 'common/src/Test/Asset/Data.php',
+ line: 54,
+ function: 'Test__Asset__Data::render_json',
+ hook: 'wp_footer',
+ priority: 10,
+ },
+ ],
+ },
+ status: 3,
+ term_status: 3,
+ forced: false,
+ term_id: 11,
+ },
+];
diff --git a/assets/src/block-validation/test/add-toolbar-button-to-block.js b/assets/src/block-validation/test/add-toolbar-button-to-block.js
new file mode 100644
index 00000000000..769ee2c1c7e
--- /dev/null
+++ b/assets/src/block-validation/test/add-toolbar-button-to-block.js
@@ -0,0 +1,195 @@
+/**
+ * External dependencies
+ */
+import { act } from 'react-dom/test-utils';
+import { noop } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { Component, render } from '@wordpress/element';
+import { dispatch } from '@wordpress/data';
+import { registerBlockType, createBlock } from '@wordpress/blocks';
+import '@wordpress/block-editor'; // Block editor data store needed.
+
+/**
+ * Internal dependencies
+ */
+import { createStore } from '../store';
+import { withAMPToolbarButton } from '../with-amp-toolbar-button';
+
+let container, block;
+let toolbarButtonWasRendered = false;
+
+const TEST_BLOCK = 'my-plugin/test-block';
+
+jest.mock( '../amp-toolbar-button', () => ( {
+ AMPToolbarButton() {
+ toolbarButtonWasRendered = true;
+
+ return null;
+ } } ) );
+
+registerBlockType( TEST_BLOCK, {
+ attributes: {},
+ save: noop,
+ category: 'widgets',
+ title: 'test block',
+} );
+
+describe( 'withAMPToolbarButton: filtering with errors', () => {
+ beforeAll( () => {
+ block = createBlock( TEST_BLOCK, {} );
+ dispatch( 'core/block-editor' ).insertBlock( block );
+
+ createStore( {
+ reviewLink: 'http://review-link.test',
+ unreviewedValidationErrors: [
+ {
+ clientId: block.clientId,
+ code: 'DISALLOWED_TAG',
+ status: 3,
+ term_id: 12,
+ title: 'Invalid script: jquery.js
',
+ type: 'js_error',
+ },
+ ],
+ validationErrors: [
+ {
+ clientId: block.clientId,
+ code: 'DISALLOWED_TAG',
+ status: 3,
+ term_id: 12,
+ title: 'Invalid script: jquery.js
',
+ type: 'js_error',
+ },
+ ],
+ } );
+ } );
+
+ beforeEach( () => {
+ container = document.createElement( 'ul' );
+ document.body.appendChild( container );
+ toolbarButtonWasRendered = false;
+ } );
+
+ afterEach( () => {
+ document.body.removeChild( container );
+ container = null;
+ } );
+
+ it( 'is filtered correctly with a class component', () => {
+ class UnfilteredComponent extends Component {
+ render() {
+ return (
+