PDF rausgenommen
This commit is contained in:
@ -0,0 +1,534 @@
|
||||
/**
|
||||
* Validates blocks for AMP compatibility.
|
||||
*
|
||||
* This uses the REST API response from saving a page to find validation errors.
|
||||
* If one exists for a block, it display it inline with a Notice component.
|
||||
*/
|
||||
|
||||
/* exported ampBlockValidation */
|
||||
/* global wp, _ */
|
||||
var ampBlockValidation = ( function() { // eslint-disable-line no-unused-vars
|
||||
'use strict';
|
||||
|
||||
var module = {
|
||||
|
||||
/**
|
||||
* Data exported from server.
|
||||
*
|
||||
* @param {Object}
|
||||
*/
|
||||
data: {
|
||||
i18n: {},
|
||||
ampValidityRestField: '',
|
||||
isSanitizationAutoAccepted: false
|
||||
},
|
||||
|
||||
/**
|
||||
* Name of the store.
|
||||
*
|
||||
* @param {string}
|
||||
*/
|
||||
storeName: 'amp/blockValidation',
|
||||
|
||||
/**
|
||||
* Holds the last states which are used for comparisons.
|
||||
*
|
||||
* @param {Object}
|
||||
*/
|
||||
lastStates: {
|
||||
noticesAreReset: false,
|
||||
validationErrors: [],
|
||||
blockOrder: [],
|
||||
blockValidationErrors: {}
|
||||
},
|
||||
|
||||
/**
|
||||
* Boot module.
|
||||
*
|
||||
* @param {Object} data - Module data.
|
||||
* @return {void}
|
||||
*/
|
||||
boot: function boot( data ) {
|
||||
module.data = data;
|
||||
|
||||
wp.i18n.setLocaleData( module.data.i18n, 'amp' );
|
||||
|
||||
wp.hooks.addFilter(
|
||||
'editor.BlockEdit',
|
||||
'amp/add-notice',
|
||||
module.conditionallyAddNotice,
|
||||
99 // eslint-disable-line
|
||||
);
|
||||
|
||||
module.store = module.registerStore();
|
||||
|
||||
wp.data.subscribe( module.handleValidationErrorsStateChange );
|
||||
},
|
||||
|
||||
/**
|
||||
* Register store.
|
||||
*
|
||||
* @return {Object} Store.
|
||||
*/
|
||||
registerStore: function registerStore() {
|
||||
return wp.data.registerStore( module.storeName, {
|
||||
reducer: function( _state, action ) {
|
||||
var state = _state || {
|
||||
blockValidationErrorsByClientId: {}
|
||||
};
|
||||
|
||||
switch ( action.type ) {
|
||||
case 'UPDATE_BLOCKS_VALIDATION_ERRORS':
|
||||
return _.extend( {}, state, {
|
||||
blockValidationErrorsByClientId: action.blockValidationErrorsByClientId
|
||||
} );
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
updateBlocksValidationErrors: function( blockValidationErrorsByClientId ) {
|
||||
return {
|
||||
type: 'UPDATE_BLOCKS_VALIDATION_ERRORS',
|
||||
blockValidationErrorsByClientId: blockValidationErrorsByClientId
|
||||
};
|
||||
}
|
||||
},
|
||||
selectors: {
|
||||
getBlockValidationErrors: function( state, clientId ) {
|
||||
return state.blockValidationErrorsByClientId[ clientId ] || [];
|
||||
}
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if AMP is enabled for this post.
|
||||
*
|
||||
* @return {boolean} Returns true when the AMP toggle is on; else, false is returned.
|
||||
*/
|
||||
isAMPEnabled: function isAMPEnabled() {
|
||||
var meta = wp.data.select( 'core/editor' ).getEditedPostAttribute( 'meta' );
|
||||
if ( meta && meta.amp_status && window.wpAmpEditor.possibleStati.includes( meta.amp_status ) ) {
|
||||
return 'enabled' === meta.amp_status;
|
||||
}
|
||||
return window.wpAmpEditor.defaultStatus;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the validate errors state change handler should wait before processing.
|
||||
*
|
||||
* @return {boolean} Whether should wait.
|
||||
*/
|
||||
waitToHandleStateChange: function waitToHandleStateChange() {
|
||||
var currentPost;
|
||||
|
||||
// @todo Gutenberg currently is not persisting isDirty state if changes are made during save request. Block order mismatch.
|
||||
// We can only align block validation errors with blocks in editor when in saved state, since only here will the blocks be aligned with the validation errors.
|
||||
if ( wp.data.select( 'core/editor' ).isEditedPostDirty() || ( ! wp.data.select( 'core/editor' ).isEditedPostDirty() && wp.data.select( 'core/editor' ).isEditedPostNew() ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wait for the current post to be set up.
|
||||
currentPost = wp.data.select( 'core/editor' ).getCurrentPost();
|
||||
if ( ! currentPost.hasOwnProperty( 'id' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle state change regarding validation errors.
|
||||
*
|
||||
* This is essentially a JS implementation of \AMP_Validation_Manager::print_edit_form_validation_status() in PHP.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
handleValidationErrorsStateChange: function handleValidationErrorsStateChange() {
|
||||
var currentPost, validationErrors, blockValidationErrors, noticeOptions, noticeMessage, blockErrorCount, ampValidity, rejectedErrors;
|
||||
|
||||
if ( ! module.isAMPEnabled() ) {
|
||||
if ( ! module.lastStates.noticesAreReset ) {
|
||||
module.lastStates.validationErrors = [];
|
||||
module.lastStates.noticesAreReset = true;
|
||||
module.resetWarningNotice();
|
||||
module.resetBlockNotices();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ( module.waitToHandleStateChange() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentPost = wp.data.select( 'core/editor' ).getCurrentPost();
|
||||
ampValidity = currentPost[ module.data.ampValidityRestField ] || {};
|
||||
|
||||
// Show all validation errors which have not been explicitly acknowledged as accepted.
|
||||
validationErrors = _.map(
|
||||
_.filter( ampValidity.results, function( result ) {
|
||||
// @todo Show VALIDATION_ERROR_ACK_REJECTED_STATUS differently since moderated?
|
||||
return (
|
||||
0 /* \AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_REJECTED_STATUS */ === result.status ||
|
||||
1 /* \AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_ACCEPTED_STATUS */ === result.status ||
|
||||
2 /* \AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACK_REJECTED_STATUS */ === result.status // eslint-disable-line no-magic-numbers
|
||||
);
|
||||
} ),
|
||||
function( result ) {
|
||||
return result.error;
|
||||
}
|
||||
);
|
||||
|
||||
// Short-circuit if there was no change to the validation errors.
|
||||
if ( ! module.didValidationErrorsChange( validationErrors ) ) {
|
||||
if ( ! validationErrors.length && ! module.lastStates.noticesAreReset ) {
|
||||
module.lastStates.noticesAreReset = true;
|
||||
module.resetWarningNotice();
|
||||
}
|
||||
return;
|
||||
}
|
||||
module.lastStates.validationErrors = validationErrors;
|
||||
module.lastStates.noticesAreReset = false;
|
||||
|
||||
// Remove any existing notice.
|
||||
module.resetWarningNotice();
|
||||
|
||||
noticeMessage = wp.i18n.sprintf(
|
||||
/* translators: %s: number of issues */
|
||||
wp.i18n._n(
|
||||
'There is %s issue from AMP validation which needs review.',
|
||||
'There are %s issues from AMP validation which need review.',
|
||||
validationErrors.length,
|
||||
'amp'
|
||||
),
|
||||
validationErrors.length
|
||||
);
|
||||
|
||||
try {
|
||||
blockValidationErrors = module.getBlocksValidationErrors();
|
||||
module.lastStates.blockValidationErrors = blockValidationErrors.byClientId;
|
||||
wp.data.dispatch( module.storeName ).updateBlocksValidationErrors( blockValidationErrors.byClientId );
|
||||
|
||||
blockErrorCount = validationErrors.length - blockValidationErrors.other.length;
|
||||
if ( blockErrorCount > 0 ) {
|
||||
noticeMessage += ' ' + wp.i18n.sprintf(
|
||||
/* translators: %s: number of block errors. */
|
||||
wp.i18n._n(
|
||||
'%s issue is directly due to content here.',
|
||||
'%s issues are directly due to content here.',
|
||||
blockErrorCount,
|
||||
'amp'
|
||||
),
|
||||
blockErrorCount
|
||||
);
|
||||
} else if ( validationErrors.length === 1 ) {
|
||||
noticeMessage += ' ' + wp.i18n.__( 'The issue is not directly due to content here.', 'amp' );
|
||||
} else {
|
||||
noticeMessage += ' ' + wp.i18n.__( 'The issues are not directly due to content here.', 'amp' );
|
||||
}
|
||||
} catch ( e ) {
|
||||
// Clear out block validation errors in case the block sand errors cannot be aligned.
|
||||
module.resetBlockNotices();
|
||||
|
||||
if ( validationErrors.length === 1 ) {
|
||||
noticeMessage += ' ' + wp.i18n.__( 'The issue may not be due to content here', 'amp' );
|
||||
} else {
|
||||
noticeMessage += ' ' + wp.i18n.__( 'Some issues may be due to content here.', 'amp' );
|
||||
}
|
||||
}
|
||||
|
||||
rejectedErrors = _.filter( ampValidity.results, function( result ) {
|
||||
return (
|
||||
0 /* \AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_NEW_REJECTED_STATUS */ === result.status ||
|
||||
2 /* \AMP_Validation_Error_Taxonomy::VALIDATION_ERROR_ACK_REJECTED_STATUS */ === result.status // eslint-disable-line no-magic-numbers
|
||||
);
|
||||
} );
|
||||
|
||||
noticeMessage += ' ';
|
||||
// Auto-acceptance is from either checking 'Automatically accept sanitization...' or from being in Native mode.
|
||||
if ( module.data.isSanitizationAutoAccepted ) {
|
||||
if ( 0 === rejectedErrors.length ) {
|
||||
noticeMessage += wp.i18n.__( 'However, your site is configured to automatically accept sanitization of the offending markup.', 'amp' );
|
||||
} else {
|
||||
noticeMessage += wp.i18n._n(
|
||||
'Your site is configured to automatically accept sanitization errors, but this error could be from when auto-acceptance was not selected, or from manually rejecting an error.',
|
||||
'Your site is configured to automatically accept sanitization errors, but these errors could be from when auto-acceptance was not selected, or from manually rejecting an error.',
|
||||
validationErrors.length,
|
||||
'amp'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
noticeMessage += wp.i18n.__( 'Non-accepted validation errors prevent AMP from being served, and the user will be redirected to the non-AMP version.', 'amp' );
|
||||
}
|
||||
|
||||
noticeOptions = {
|
||||
id: 'amp-errors-notice'
|
||||
};
|
||||
if ( ampValidity.review_link ) {
|
||||
noticeOptions.actions = [
|
||||
{
|
||||
label: wp.i18n.__( 'Review issues', 'amp' ),
|
||||
url: ampValidity.review_link
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Display notice if there were validation errors.
|
||||
if ( validationErrors.length > 0 ) {
|
||||
wp.data.dispatch( 'core/notices' ).createNotice( 'warning', noticeMessage, noticeOptions );
|
||||
}
|
||||
|
||||
module.validationWarningNoticeId = noticeOptions.id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the validation errors have changed.
|
||||
*
|
||||
* @param {Object[]} validationErrors A list of validation errors.
|
||||
* @return {boolean|*} Returns true when the validation errors change.
|
||||
*/
|
||||
didValidationErrorsChange: function didValidationErrorsChange( validationErrors ) {
|
||||
if ( module.areBlocksOutOfSync() ) {
|
||||
module.lastStates.validationErrors = [];
|
||||
}
|
||||
|
||||
return (
|
||||
module.lastStates.validationErrors.length !== validationErrors.length ||
|
||||
( validationErrors && ! _.isEqual( module.lastStates.validationErrors, validationErrors ) )
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the block order is out of sync.
|
||||
*
|
||||
* Block change on page load and can get out of sync during normal editing and saving processes. This method gives a check to determine if an "out of sync" condition occurred.
|
||||
*
|
||||
* @return {boolean} Whether out of sync.
|
||||
*/
|
||||
areBlocksOutOfSync: function areBlocksOutOfSync() {
|
||||
var blockOrder = wp.data.select( 'core/editor' ).getBlockOrder();
|
||||
if ( module.lastStates.blockOrder.length !== blockOrder.length || ! _.isEqual( module.lastStates.blockOrder, blockOrder ) ) {
|
||||
module.lastStates.blockOrder = blockOrder;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets the validation warning notice.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
resetWarningNotice: function resetWarningNotice() {
|
||||
if ( module.validationWarningNoticeId ) {
|
||||
wp.data.dispatch( 'core/notices' ).removeNotice( module.validationWarningNoticeId );
|
||||
module.validationWarningNoticeId = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets the block level validation errors.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
resetBlockNotices: function resetBlockNotices() {
|
||||
wp.data.dispatch( module.storeName ).updateBlocksValidationErrors( {} );
|
||||
},
|
||||
|
||||
/**
|
||||
* Get flattened block order.
|
||||
*
|
||||
* @param {Object[]} blocks - List of blocks which maty have nested blocks inside them.
|
||||
* @return {string[]} Block IDs in flattened order.
|
||||
*/
|
||||
getFlattenedBlockOrder: function getFlattenedBlockOrder( blocks ) {
|
||||
var blockOrder = [];
|
||||
_.each( blocks, function( block ) {
|
||||
blockOrder.push( block.clientId );
|
||||
if ( block.innerBlocks.length > 0 ) {
|
||||
Array.prototype.push.apply( blockOrder, module.getFlattenedBlockOrder( block.innerBlocks ) );
|
||||
}
|
||||
} );
|
||||
return blockOrder;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update blocks' validation errors in the store.
|
||||
*
|
||||
* @return {Object} Validation errors grouped by block ID other ones.
|
||||
*/
|
||||
getBlocksValidationErrors: function getBlocksValidationErrors() {
|
||||
var acceptedStatus, blockValidationErrorsByClientId, editorSelect, currentPost, blockOrder, validationErrors, otherValidationErrors;
|
||||
acceptedStatus = 3; // eslint-disable-line no-magic-numbers
|
||||
editorSelect = wp.data.select( 'core/editor' );
|
||||
currentPost = editorSelect.getCurrentPost();
|
||||
validationErrors = _.map(
|
||||
_.filter( currentPost[ module.data.ampValidityRestField ].results, function( result ) {
|
||||
return result.term_status !== acceptedStatus; // If not accepted by the user.
|
||||
} ),
|
||||
function( result ) {
|
||||
return result.error;
|
||||
}
|
||||
);
|
||||
blockOrder = module.getFlattenedBlockOrder( editorSelect.getBlocks() );
|
||||
|
||||
otherValidationErrors = [];
|
||||
blockValidationErrorsByClientId = {};
|
||||
_.each( blockOrder, function( clientId ) {
|
||||
blockValidationErrorsByClientId[ clientId ] = [];
|
||||
} );
|
||||
|
||||
_.each( validationErrors, function( validationError ) {
|
||||
var i, source, clientId, block, matched;
|
||||
if ( ! validationError.sources ) {
|
||||
otherValidationErrors.push( validationError );
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the inner-most nested block source only; ignore any nested blocks.
|
||||
matched = false;
|
||||
for ( i = validationError.sources.length - 1; 0 <= i; i-- ) {
|
||||
source = validationError.sources[ i ];
|
||||
|
||||
// Skip sources that are not for blocks.
|
||||
if ( ! source.block_name || _.isUndefined( source.block_content_index ) || currentPost.id !== source.post_id ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Look up the block ID by index, assuming the blocks of content in the editor are the same as blocks rendered on frontend.
|
||||
clientId = blockOrder[ source.block_content_index ];
|
||||
if ( _.isUndefined( clientId ) ) {
|
||||
throw new Error( 'undefined_block_index' );
|
||||
}
|
||||
|
||||
// Sanity check that block exists for clientId.
|
||||
block = editorSelect.getBlock( clientId );
|
||||
if ( ! block ) {
|
||||
throw new Error( 'block_lookup_failure' );
|
||||
}
|
||||
|
||||
// Check the block type in case a block is dynamically added/removed via the_content filter to cause alignment error.
|
||||
if ( block.name !== source.block_name ) {
|
||||
throw new Error( 'ordered_block_alignment_mismatch' );
|
||||
}
|
||||
|
||||
blockValidationErrorsByClientId[ clientId ].push( validationError );
|
||||
matched = true;
|
||||
|
||||
// Stop looking for sources, since we aren't looking for parent blocks.
|
||||
break;
|
||||
}
|
||||
|
||||
if ( ! matched ) {
|
||||
otherValidationErrors.push( validationError );
|
||||
}
|
||||
} );
|
||||
|
||||
return {
|
||||
byClientId: blockValidationErrorsByClientId,
|
||||
other: otherValidationErrors
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get message for validation error.
|
||||
*
|
||||
* @param {Object} validationError - Validation error.
|
||||
* @param {string} validationError.code - Validation error code.
|
||||
* @param {string} [validationError.node_name] - Node name.
|
||||
* @param {string} [validationError.message] - Validation error message.
|
||||
* @return {wp.element.Component[]|string[]} Validation error message.
|
||||
*/
|
||||
getValidationErrorMessage: function getValidationErrorMessage( validationError ) {
|
||||
if ( validationError.message ) {
|
||||
return validationError.message;
|
||||
}
|
||||
if ( 'invalid_element' === validationError.code && validationError.node_name ) {
|
||||
return [
|
||||
wp.i18n.__( 'Invalid element: ' ),
|
||||
wp.element.createElement( 'code', { key: 'name' }, validationError.node_name )
|
||||
];
|
||||
} else if ( 'invalid_attribute' === validationError.code && validationError.node_name ) {
|
||||
return [
|
||||
wp.i18n.__( 'Invalid attribute: ' ),
|
||||
wp.element.createElement( 'code', { key: 'name' }, validationError.parent_name ? wp.i18n.sprintf( '%s[%s]', validationError.parent_name, validationError.node_name ) : validationError.node_name )
|
||||
];
|
||||
}
|
||||
return [
|
||||
wp.i18n.__( 'Error code: ', 'amp' ),
|
||||
wp.element.createElement( 'code', { key: 'name' }, validationError.code || wp.i18n.__( 'unknown' ) )
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Wraps the edit() method of a block, and conditionally adds a Notice.
|
||||
*
|
||||
* @param {Function} BlockEdit - The original edit() method of the block.
|
||||
* @return {Function} The edit() method, conditionally wrapped in a notice for AMP validation error(s).
|
||||
*/
|
||||
conditionallyAddNotice: function conditionallyAddNotice( BlockEdit ) {
|
||||
return function( ownProps ) {
|
||||
var validationErrors,
|
||||
mergedProps;
|
||||
function AmpNoticeBlockEdit( props ) {
|
||||
var edit, details;
|
||||
edit = wp.element.createElement(
|
||||
BlockEdit,
|
||||
props
|
||||
);
|
||||
|
||||
if ( 0 === props.ampBlockValidationErrors.length ) {
|
||||
return edit;
|
||||
}
|
||||
|
||||
details = wp.element.createElement( 'details', { className: 'amp-block-validation-errors' }, [
|
||||
wp.element.createElement( 'summary', { key: 'summary', className: 'amp-block-validation-errors__summary' }, wp.i18n.sprintf(
|
||||
wp.i18n._n(
|
||||
'There is %s issue from AMP validation.',
|
||||
'There are %s issues from AMP validation.',
|
||||
props.ampBlockValidationErrors.length,
|
||||
'amp'
|
||||
),
|
||||
props.ampBlockValidationErrors.length
|
||||
) ),
|
||||
wp.element.createElement(
|
||||
'ul',
|
||||
{ key: 'list', className: 'amp-block-validation-errors__list' },
|
||||
_.map( props.ampBlockValidationErrors, function( error, key ) {
|
||||
return wp.element.createElement( 'li', { key: key }, module.getValidationErrorMessage( error ) );
|
||||
} )
|
||||
)
|
||||
] );
|
||||
|
||||
return wp.element.createElement(
|
||||
wp.element.Fragment, {},
|
||||
wp.element.createElement(
|
||||
wp.components.Notice,
|
||||
{
|
||||
status: 'warning',
|
||||
isDismissible: false
|
||||
},
|
||||
details
|
||||
),
|
||||
edit
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! module.lastStates.blockValidationErrors[ ownProps.clientId ] ) {
|
||||
validationErrors = wp.data.select( module.storeName ).getBlockValidationErrors( ownProps.clientId );
|
||||
module.lastStates.blockValidationErrors[ ownProps.clientId ] = validationErrors;
|
||||
}
|
||||
|
||||
mergedProps = _.extend( {}, ownProps, {
|
||||
ampBlockValidationErrors: module.lastStates.blockValidationErrors[ ownProps.clientId ]
|
||||
} );
|
||||
|
||||
return AmpNoticeBlockEdit( mergedProps );
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return module;
|
||||
}() );
|
Reference in New Issue
Block a user