diff --git a/amp.php b/amp.php index dbd4f4577c2..6b7ca698f50 100644 --- a/amp.php +++ b/amp.php @@ -144,6 +144,10 @@ function amp_init() { add_rewrite_endpoint( amp_get_slug(), EP_PERMALINK ); + add_filter( 'allowed_redirect_hosts', array( 'AMP_HTTP', 'filter_allowed_redirect_hosts' ) ); + AMP_HTTP::purge_amp_query_vars(); + AMP_HTTP::send_cors_headers(); + AMP_HTTP::handle_xhr_request(); AMP_Theme_Support::init(); AMP_Validation_Manager::init(); AMP_Post_Type_Support::add_post_type_support(); diff --git a/assets/css/amp-editor-story-blocks.css b/assets/css/amp-editor-story-blocks.css index 68fe179ab18..92050bfd64e 100644 --- a/assets/css/amp-editor-story-blocks.css +++ b/assets/css/amp-editor-story-blocks.css @@ -160,3 +160,7 @@ div[data-amp-position="lower-third"] { .editor-selectors .is-selected.component-editor__selector button { text-decoration: underline; } + +.post-type-amp_story .components-range-control__number { + width: 60px; +} diff --git a/assets/js/amp-block-validation.js b/assets/js/amp-block-validation.js index aca64843fcd..f90eb95de2e 100644 --- a/assets/js/amp-block-validation.js +++ b/assets/js/amp-block-validation.js @@ -124,7 +124,7 @@ var ampBlockValidation = ( function() { // eslint-disable-line no-unused-vars // @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' ).isCleanNewPost() ) { + if ( wp.data.select( 'core/editor' ).isEditedPostDirty() || ( ! wp.data.select( 'core/editor' ).isEditedPostDirty() && wp.data.select( 'core/editor' ).isEditedPostNew() ) ) { return true; } diff --git a/assets/js/amp-story-editor-blocks.js b/assets/js/amp-story-editor-blocks.js index e2032db7110..f0a3917e14c 100644 --- a/assets/js/amp-story-editor-blocks.js +++ b/assets/js/amp-story-editor-blocks.js @@ -14,6 +14,7 @@ var ampStoryEditorBlocks = ( function() { // eslint-disable-line no-unused-vars */ data: { allowedBlocks: [ + 'core/button', 'core/code', 'core/embed', 'core/image', @@ -26,6 +27,19 @@ var ampStoryEditorBlocks = ( function() { // eslint-disable-line no-unused-vars 'core/verse', 'core/video' ], + blockTagMapping: { + 'core/button': 'div.wp-block-button', + 'core/code': 'pre', + 'core/embed': 'figure', + 'core/image': 'figure.wp-block-image', + 'core/paragraph': 'p', + 'core/preformatted': 'pre', + 'core/pullquote': 'blockquote', + 'core/quote': 'blockquote', + 'core/table': 'table', + 'core/verse': 'pre', + 'core/video': 'figure' + }, ampStoryPositionOptions: [ { value: 'upper-third', @@ -39,6 +53,84 @@ var ampStoryEditorBlocks = ( function() { // eslint-disable-line no-unused-vars value: 'lower-third', label: __( 'Lower Third', 'amp' ) } + ], + ampAnimationTypeOptions: [ + { + value: '', + label: __( 'None', 'amp' ) + }, + { + value: 'drop', + label: __( 'Drop', 'amp' ) + }, + { + value: 'fade-in', + label: __( 'Fade In', 'amp' ) + }, + { + value: 'fly-in-bottom', + label: __( 'Fly In Bottom', 'amp' ) + }, + { + value: 'fly-in-left', + label: __( 'Fly In Left', 'amp' ) + }, + { + value: 'fly-in-right', + label: __( 'Fly In Right', 'amp' ) + }, + { + value: 'fly-in-top', + label: __( 'Fly In Top', 'amp' ) + }, + { + value: 'pulse', + label: __( 'Pulse', 'amp' ) + }, + { + value: 'rotate-in-left', + label: __( 'Rotate In Left', 'amp' ) + }, + { + value: 'rotate-in-right', + label: __( 'Rotate In Right', 'amp' ) + }, + { + value: 'twirl-in', + label: __( 'Twirl In', 'amp' ) + }, + { + value: 'whoosh-in-left', + label: __( 'Whoosh In Left', 'amp' ) + }, + { + value: 'whoosh-in-right', + label: __( 'Whoosh In Right', 'amp' ) + }, + { + value: 'pan-left', + label: __( 'Pan Left', 'amp' ) + }, + { + value: 'pan-right', + label: __( 'Pan Right', 'amp' ) + }, + { + value: 'pan-down', + label: __( 'Pan Down', 'amp' ) + }, + { + value: 'pan-up', + label: __( 'Pan Up', 'amp' ) + }, + { + value: 'zoom-in', + label: __( 'Zoom In', 'amp' ) + }, + { + value: 'zoom-out', + label: __( 'Zoom Out', 'amp' ) + } ] } }; @@ -102,6 +194,7 @@ var ampStoryEditorBlocks = ( function() { // eslint-disable-line no-unused-vars ); }; }; + /** * Add extra attributes to save to DB. * @@ -119,6 +212,16 @@ var ampStoryEditorBlocks = ( function() { // eslint-disable-line no-unused-vars if ( attributes.ampStoryPosition ) { ampAttributes[ 'grid-area' ] = attributes.ampStoryPosition; } + if ( attributes.ampAnimationType ) { + ampAttributes[ 'animate-in' ] = attributes.ampAnimationType; + + if ( attributes.ampAnimationDelay ) { + ampAttributes[ 'animate-in-delay' ] = attributes.ampAnimationDelay; + } + if ( attributes.ampAnimationDuration ) { + ampAttributes[ 'animate-in-duration' ] = attributes.ampAnimationDuration; + } + } return _.extend( ampAttributes, props ); }; @@ -131,7 +234,7 @@ var ampStoryEditorBlocks = ( function() { // eslint-disable-line no-unused-vars * @return {Object} Settings. */ component.addAMPAttributes = function addAMPAttributes( settings, name ) { - // Add the "thirds" template position option. + // Add the "thirds" template position option and animation settings. if ( -1 !== component.data.allowedBlocks.indexOf( name ) ) { if ( ! settings.attributes ) { settings.attributes = {}; @@ -139,6 +242,42 @@ var ampStoryEditorBlocks = ( function() { // eslint-disable-line no-unused-vars settings.attributes.ampStoryPosition = { type: 'string' }; + + // Define selector according to mappings. + if ( _.has( component.data.blockTagMapping, name ) ) { + settings.attributes.ampAnimationType = { + type: 'string', + source: 'attribute', + selector: component.data.blockTagMapping[ name ], + attribute: 'animate-in' + }; + settings.attributes.ampAnimationDelay = { + type: 'string', + source: 'attribute', + selector: component.data.blockTagMapping[ name ], + attribute: 'animate-in-delay', + default: '0ms' + }; + settings.attributes.ampAnimationDuration = { + type: 'string', + source: 'attribute', + selector: component.data.blockTagMapping[ name ], + attribute: 'animate-in-duration', + default: '0ms' + }; + } else if ( 'core/list' === name ) { + settings.attributes.ampAnimationType = { + type: 'string' + }; + settings.attributes.ampAnimationDelay = { + type: 'number', + default: 0 + }; + settings.attributes.ampAnimationDuration = { + type: 'number', + default: 0 + }; + } } return settings; }; @@ -173,7 +312,7 @@ var ampStoryEditorBlocks = ( function() { // eslint-disable-line no-unused-vars } parentBlock = select.getBlock( parentClientId ); - if ( 'amp/amp-story-grid-layer' !== parentBlock.name ) { + if ( 'amp/amp-story-grid-layer' !== parentBlock.name && 'amp/amp-story-cta-layer' !== parentBlock.name ) { // Return original. return [ el( BlockEdit, _.extend( { @@ -183,27 +322,28 @@ var ampStoryEditorBlocks = ( function() { // eslint-disable-line no-unused-vars } if ( 'thirds' !== parentBlock.attributes.template ) { - // Return original. - return [ - el( BlockEdit, _.extend( { - key: 'original' - }, props ) ) - ]; + inspectorControls = el( InspectorControls, { key: 'inspector' }, + el( PanelBody, { title: __( 'AMP Story Settings', 'amp' ), key: 'amp-story' }, + component.getAnimationControls( props ) + ) + ); + } else { + inspectorControls = el( InspectorControls, { key: 'inspector' }, + el( PanelBody, { title: __( 'AMP Story Settings', 'amp' ), key: 'amp-story' }, + el( SelectControl, { + key: 'position', + label: __( 'Placement', 'amp' ), + value: attributes.ampStoryPosition, + options: component.data.ampStoryPositionOptions, + onChange: function( value ) { + props.setAttributes( { ampStoryPosition: value } ); + } + } ), + component.getAnimationControls( props ) + ) + ); } - inspectorControls = el( InspectorControls, { key: 'inspector' }, - el( PanelBody, { title: __( 'AMP Story Settings', 'amp' ) }, - el( SelectControl, { - label: __( 'Placement', 'amp' ), - value: attributes.ampStoryPosition, - options: component.data.ampStoryPositionOptions, - onChange: function( value ) { - props.setAttributes( { ampStoryPosition: value } ); - } - } ) - ) - ); - return [ inspectorControls, el( BlockEdit, _.extend( { @@ -213,5 +353,46 @@ var ampStoryEditorBlocks = ( function() { // eslint-disable-line no-unused-vars }; }; + component.getAnimationControls = function getAnimationControls( props ) { + var RangeControl = wp.components.RangeControl, + el = wp.element.createElement, + SelectControl = wp.components.SelectControl, + attributes = props.attributes; + + return [ + el( SelectControl, { + key: 'animation-type', + label: __( 'Animation type', 'amp' ), + value: attributes.ampAnimationType, + options: component.data.ampAnimationTypeOptions, + onChange: function( value ) { + props.setAttributes( { ampAnimationType: value } ); + } + } ), + el( RangeControl, { + key: 'animation-duration', + label: __( 'Animation duration (ms)', 'amp' ), + value: parseInt( attributes.ampAnimationDuration ), + min: 0, + max: 5000, + onChange: function( value ) { + var msValue = value + 'ms'; + props.setAttributes( { ampAnimationDuration: msValue } ); + } + } ), + el( RangeControl, { + key: 'animation-delay', + label: __( 'Animation delay (ms)', 'amp' ), + value: parseInt( attributes.ampAnimationDelay ), + min: 0, + max: 5000, + onChange: function( value ) { + var msValue = value + 'ms'; + props.setAttributes( { ampAnimationDelay: msValue } ); + } + } ) + ]; + }; + return component; }() ); diff --git a/bin/amphtml-update.py b/bin/amphtml-update.py index 4c468fe9932..bf6071200c4 100644 --- a/bin/amphtml-update.py +++ b/bin/amphtml-update.py @@ -100,7 +100,7 @@ def GeneratePHP(out_dir): logging.info('entering ...') assert re.match(r'^[a-zA-Z_\-0-9]+$', out_dir), 'bad out_dir: %s' % out_dir - allowed_tags, attr_lists, versions = ParseRules(out_dir) + allowed_tags, attr_lists, reference_points, versions = ParseRules(out_dir) #Generate the output out = [] @@ -109,6 +109,7 @@ def GeneratePHP(out_dir): GenerateAllowedTagsPHP(out, allowed_tags) GenerateLayoutAttributesPHP(out, attr_lists) GenerateGlobalAttributesPHP(out, attr_lists) + GenerateReferencePointsPHP(out, reference_points) GenerateFooterPHP(out) # join out array into a single string and remove unneeded whitespace @@ -188,6 +189,15 @@ def GenerateGlobalAttributesPHP(out, attr_lists): out.append('') logging.info('... done') +def GenerateReferencePointsPHP(out, reference_points): + logging.info('entering ...') + + # Output the reference points. + out.append('') + out.append('\tprivate static $reference_points = %s;' % Phpize( reference_points, 1 ).lstrip() ) + out.append('') + logging.info('... done') + def GenerateFooterPHP(out): logging.info('entering ...') @@ -219,6 +229,20 @@ def GenerateFooterPHP(out): return null; } + /** + * Get reference point spec. + * + * @since 1.0 + * @param string $tag_spec_name Tag spec name. + * @return array|null Reference point spec, or null if does not exist. + */ + public static function get_reference_point_spec( $tag_spec_name ) { + if ( isset( self::$reference_points[ $tag_spec_name ] ) ) { + return self::$reference_points[ $tag_spec_name ]; + } + return null; + } + /** * Get list of globally-allowed attributes. * @@ -258,6 +282,7 @@ def ParseRules(out_dir): allowed_tags = {} attr_lists = {} + reference_points = {} versions = {} specfile='%s/validator.protoascii' % out_dir @@ -301,14 +326,15 @@ def ParseRules(out_dir): if tag_spec.HasField('mandatory_parent') and tag_spec.mandatory_parent in mandatory_parent_blacklist and tag_spec.tag_name != 'HTML': continue - # Ignore the special $REFERENCE_POINT tag - if '$REFERENCE_POINT' == tag_spec.tag_name: - continue - # Ignore deprecated tags if tag_spec.HasField('deprecation'): continue + # Handle the special $REFERENCE_POINT tag + if '$REFERENCE_POINT' == tag_spec.tag_name: + reference_points[ tag_spec.spec_name ] = GetTagSpec(tag_spec, attr_lists) + continue + # If we made it here, then start adding the tag_spec if tag_spec.tag_name.lower() not in allowed_tags: tag_list = [] @@ -322,7 +348,7 @@ def ParseRules(out_dir): allowed_tags[UnicodeEscape(tag_spec.tag_name).lower()] = tag_list logging.info('... done') - return allowed_tags, attr_lists, versions + return allowed_tags, attr_lists, reference_points, versions def GetTagSpec(tag_spec, attr_lists): @@ -400,6 +426,16 @@ def GetTagRules(tag_spec): requires_extension_list.append(requires_extension) tag_rules['requires_extension'] = requires_extension_list + if hasattr(tag_spec, 'reference_points') and len( tag_spec.reference_points ) != 0: + tag_reference_points = {} + for reference_point_spec in tag_spec.reference_points: + tag_reference_points[ reference_point_spec.tag_spec_name ] = { + "mandatory": reference_point_spec.mandatory, + "unique": reference_point_spec.unique + } + if len( tag_reference_points ) > 0: + tag_rules['reference_points'] = tag_reference_points + if hasattr(tag_spec, 'also_requires_tag_warning') and len( tag_spec.also_requires_tag_warning ) != 0: also_requires_tag_warning_list = [] for also_requires_tag_warning in tag_spec.also_requires_tag_warning: diff --git a/blocks/amp-story/amp-story-cta-layer.js b/blocks/amp-story/amp-story-cta-layer.js index 9677b9ee3ec..d4db3fe4d3e 100644 --- a/blocks/amp-story/amp-story-cta-layer.js +++ b/blocks/amp-story/amp-story-cta-layer.js @@ -1,12 +1,16 @@ +import { getAmpStoryAnimationControls } from './helpers'; + const { __ } = wp.i18n; const { registerBlockType } = wp.blocks; const { + InspectorControls, InnerBlocks } = wp.editor; const { - Notice + Notice, + PanelBody } = wp.components; const { Component } = wp.element; @@ -50,6 +54,30 @@ export default registerBlockType( category: 'layout', icon: 'grid-view', parent: [ 'amp/amp-story-page' ], + + attributes: { + animationType: { + type: 'string', + source: 'attribute', + selector: 'amp-story-cta-layer', + attribute: 'animate-in' + }, + animationDuration: { + type: 'string', + source: 'attribute', + selector: 'amp-story-cta-layer', + attribute: 'animate-in-duration', + default: '0ms' + }, + animationDelay: { + type: 'string', + source: 'attribute', + selector: 'amp-story-cta-layer', + attribute: 'animate-in-delay', + default: '0ms' + } + }, + inserter: false, /* @@ -99,9 +127,16 @@ export default registerBlockType( { __( 'Multiple CTA Layers are not allowed. Please remove all but one.', 'amp' ) } ); } - return ( + return [ + + + { + getAmpStoryAnimationControls( this.props.setAttributes, this.props.attributes ) + } + + , - ); + ]; } hasMoreThanOneCtaBlock() { @@ -120,8 +155,19 @@ export default registerBlockType( }, save( { attributes } ) { + let layerProps = {}; + if ( attributes.animationType ) { + layerProps[ 'animate-in' ] = attributes.animationType; + + if ( attributes.animationDelay ) { + layerProps[ 'animate-in-delay' ] = attributes.animationDelay; + } + if ( attributes.animationDuration ) { + layerProps[ 'animate-in-duration' ] = attributes.animationDuration; + } + } return ( - + ); diff --git a/blocks/amp-story/amp-story-grid-layer.js b/blocks/amp-story/amp-story-grid-layer.js index 51c4ccaaa79..e3dcd0d648a 100644 --- a/blocks/amp-story/amp-story-grid-layer.js +++ b/blocks/amp-story/amp-story-grid-layer.js @@ -1,3 +1,5 @@ +import { getAmpStoryAnimationControls } from './helpers'; + const { __ } = wp.i18n; const { registerBlockType @@ -7,7 +9,8 @@ const { InnerBlocks } = wp.editor; const { - SelectControl + SelectControl, + PanelBody } = wp.components; const ALLOWED_BLOCKS = [ @@ -59,6 +62,26 @@ export default registerBlockType( selector: 'amp-story-grid-layer', attribute: 'template', default: 'vertical' + }, + animationType: { + type: 'string', + source: 'attribute', + selector: 'amp-story-grid-layer', + attribute: 'animate-in' + }, + animationDuration: { + type: 'string', + source: 'attribute', + selector: 'amp-story-grid-layer', + attribute: 'animate-in-duration', + default: '0ms' + }, + animationDelay: { + type: 'string', + source: 'attribute', + selector: 'amp-story-grid-layer', + attribute: 'animate-in-delay', + default: '0ms' } }, @@ -73,13 +96,13 @@ export default registerBlockType( */ edit( props ) { - const { setAttributes } = props; + const { setAttributes, attributes } = props; return [ ( setAttributes( { template: value } ) ) } /> + + { + getAmpStoryAnimationControls( setAttributes, attributes ) + } + ,
@@ -108,8 +136,22 @@ export default registerBlockType( }, save( { attributes } ) { + let layerProps = { + template: attributes.template + }; + if ( attributes.animationType ) { + layerProps[ 'animate-in' ] = attributes.animationType; + + if ( attributes.animationDelay ) { + layerProps[ 'animate-in-delay' ] = attributes.animationDelay; + } + if ( attributes.animationDuration ) { + layerProps[ 'animate-in-duration' ] = attributes.animationDuration; + } + } + return ( - + ); diff --git a/blocks/amp-story/helpers.js b/blocks/amp-story/helpers.js new file mode 100644 index 00000000000..0e1c178dff2 --- /dev/null +++ b/blocks/amp-story/helpers.js @@ -0,0 +1,123 @@ +const { __ } = wp.i18n; +const { + SelectControl, + RangeControl +} = wp.components; + +/** + * Animation controls for AMP Story layout blocks'. + * + * @param {Function} setAttributes Set Attributes. + * @param {Object} attributes Props. + * @return {[XML,*,XML,*,XML]} Controls. + */ +export function getAmpStoryAnimationControls( setAttributes, attributes ) { + return [ + ( setAttributes( { animationType: value } ) ) } + />, + { + value = value + 'ms'; + setAttributes( { animationDuration: value } ); + } } + min='0' + max='5000' + />, + { + value = value + 'ms'; + setAttributes( { animationDelay: value } ); + } } + min='0' + max='5000' + /> + ]; +} diff --git a/includes/amp-helper-functions.php b/includes/amp-helper-functions.php index 8202588eac0..e5be1fae758 100644 --- a/includes/amp-helper-functions.php +++ b/includes/amp-helper-functions.php @@ -897,3 +897,17 @@ function amp_print_schemaorg_metadata() { 'includes/admin/class-amp-editor-blocks', 'AMP_Theme_Support' => 'includes/class-amp-theme-support', 'AMP_Story_Post_Type' => 'includes/class-amp-story-post-type', - 'AMP_Response_Headers' => 'includes/class-amp-response-headers', + 'AMP_HTTP' => 'includes/class-amp-http', 'AMP_Comment_Walker' => 'includes/class-amp-comment-walker', 'AMP_Template_Customizer' => 'includes/admin/class-amp-customizer', 'AMP_Post_Meta_Box' => 'includes/admin/class-amp-post-meta-box', diff --git a/includes/class-amp-http.php b/includes/class-amp-http.php new file mode 100644 index 00000000000..2df21e8882f --- /dev/null +++ b/includes/class-amp-http.php @@ -0,0 +1,438 @@ + true, + 'status_code' => null, + ), + $args + ); + + self::$headers_sent[] = array_merge( compact( 'name', 'value' ), $args ); + if ( headers_sent() ) { + return false; + } + + header( + sprintf( '%s: %s', $name, $value ), + $args['replace'], + $args['status_code'] + ); + return true; + } + + /** + * Send Server-Timing header. + * + * If WP_DEBUG is not enabled and an admin user (who can manage_options) is not logged-in, the Server-Header will not be sent. + * + * @since 1.0 + * + * @param string $name Name. + * @param float $duration Duration. If negative, will be added to microtime( true ). Optional. + * @param string $description Description. Optional. + * @return bool Return value of send_header call. If WP_DEBUG is not enabled or admin user (who can manage_options) is not logged-in, this will always return false. + */ + public static function send_server_timing( $name, $duration = null, $description = null ) { + if ( ! WP_DEBUG && ! current_user_can( 'manage_options' ) ) { + return false; + } + $value = $name; + if ( isset( $description ) ) { + $value .= sprintf( ';desc="%s"', str_replace( array( '\\', '"' ), '', substr( $description, 0, 100 ) ) ); + } + if ( isset( $duration ) ) { + if ( $duration < 0 ) { + $duration = microtime( true ) + $duration; + } + $value .= sprintf( ';dur=%f', $duration * 1000 ); + } + return self::send_header( 'Server-Timing', $value, array( 'replace' => false ) ); + } + + /** + * Remove query vars that come in requests such as for amp-live-list. + * + * WordPress should generally not respond differently to requests when these parameters + * are present. In some cases, when a query param such as __amp_source_origin is present + * then it would normally get included into pagination links generated by get_pagenum_link(). + * The whitelist sanitizer empties out links that contain this string as it matches the + * blacklisted_value_regex. So by preemptively scrubbing any reference to these query vars + * we can ensure that WordPress won't end up referencing them in any way. + * + * @since 0.7 + * @since 1.0 Moved to AMP_HTTP class. + */ + public static function purge_amp_query_vars() { + $query_vars = array( + '__amp_source_origin', + '_wp_amp_action_xhr_converted', + 'amp_latest_update_time', + 'amp_last_check_time', + ); + + // Scrub input vars. + foreach ( $query_vars as $query_var ) { + if ( ! isset( $_GET[ $query_var ] ) ) { // phpcs:ignore + continue; + } + self::$purged_amp_query_vars[ $query_var ] = wp_unslash( $_GET[ $query_var ] ); // phpcs:ignore + unset( $_REQUEST[ $query_var ], $_GET[ $query_var ] ); + $scrubbed = true; + } + + if ( isset( $scrubbed ) ) { + $build_query = function ( $query ) use ( $query_vars ) { + $pattern = '/^(' . join( '|', $query_vars ) . ')(?==|$)/'; + $pairs = array(); + foreach ( explode( '&', $query ) as $pair ) { + if ( ! preg_match( $pattern, $pair ) ) { + $pairs[] = $pair; + } + } + + return join( '&', $pairs ); + }; + + // Scrub QUERY_STRING. + if ( ! empty( $_SERVER['QUERY_STRING'] ) ) { + $_SERVER['QUERY_STRING'] = $build_query( $_SERVER['QUERY_STRING'] ); + } + + // Scrub REQUEST_URI. + if ( ! empty( $_SERVER['REQUEST_URI'] ) ) { + list( $path, $query ) = explode( '?', $_SERVER['REQUEST_URI'], 2 ); + + $pairs = $build_query( $query ); + $_SERVER['REQUEST_URI'] = $path; + if ( ! empty( $pairs ) ) { + $_SERVER['REQUEST_URI'] .= "?{$pairs}"; + } + } + } + } + + /** + * Filter the allowed redirect hosts to include AMP caches. + * + * @since 1.0 + * + * @param array $allowed_hosts Allowed hosts. + * @return array Allowed redirect hosts. + */ + public static function filter_allowed_redirect_hosts( $allowed_hosts ) { + return array_merge( $allowed_hosts, self::get_amp_cache_hosts() ); + } + + /** + * Get list of AMP cache hosts (that is, CORS origins). + * + * @since 1.0 + * @link https://www.ampproject.org/docs/fundamentals/amp-cors-requests#1)-allow-requests-for-specific-cors-origins + * + * @return array AMP cache hosts. + */ + public static function get_amp_cache_hosts() { + $hosts = array(); + + // Google AMP Cache (legacy). + $hosts[] = 'cdn.ampproject.org'; + + // From the publisher’s own origins. + $domains = array_unique( array( + wp_parse_url( site_url(), PHP_URL_HOST ), + wp_parse_url( home_url(), PHP_URL_HOST ), + ) ); + + /* + * From AMP docs: + * "When possible, the Google AMP Cache will create a subdomain for each AMP document's domain by first converting it + * from IDN (punycode) to UTF-8. The caches replaces every - (dash) with -- (2 dashes) and replace every . (dot) with + * - (dash). For example, pub.com will map to pub-com.cdn.ampproject.org." + */ + foreach ( $domains as $domain ) { + if ( function_exists( 'idn_to_utf8' ) ) { + if ( version_compare( PHP_VERSION, '5.4', '>=' ) ) { + $domain = idn_to_utf8( $domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 ); // phpcs:ignore PHPCompatibility.PHP.NewFunctionParameters.idn_to_utf8_variantFound, PHPCompatibility.PHP.NewConstants.intl_idna_variant_uts46Found + } else { + $domain = idn_to_utf8( $domain ); + } + } + $subdomain = str_replace( '-', '--', $domain ); + $subdomain = str_replace( '.', '-', $subdomain ); + + // Google AMP Cache subdomain. + $hosts[] = sprintf( '%s.cdn.ampproject.org', $subdomain ); + + // Cloudflare AMP Cache. + $hosts[] = sprintf( '%s.amp.cloudflare.com', $subdomain ); + } + + return $hosts; + } + + /** + * Send cors headers. + * + * From the AMP docs: + * Restrict requests to source origins + * In all fetch requests, the AMP Runtime passes the "__amp_source_origin" query parameter, which contains + * the value of the source origin (for example, "https://publisher1.com"). + * + * To restrict requests to only source origins, check that the value of the "__amp_source_origin" parameter + * is within a set of the Publisher's own origins. + * + * Access-Control-Allow-Origin: + * This header is a W3 CORS Spec requirement, where origin refers to the requesting origin that was allowed + * via the CORS Origin request header (for example, "https://.cdn.ampproject.org"). + * + * Although the W3 CORS spec allows the value of * to be returned in the response, for improved security, you should: + * + * - If the Origin header is present, validate and echo the value of the Origin header. + * - If the Origin header isn't present, validate and echo the value of the "__amp_source_origin". + * + * (Otherwise, no Access-Control-Allow-Origin header is sent.) + * + * AMP-Access-Control-Allow-Source-Origin: + * This header allows the specified source-origin to read the authorization response. The source-origin is + * the value specified and verified in the "__amp_source_origin" URL parameter (for example, "https://publisher1.com"). + * + * Access-Control-Expose-Headers: AMP-Access-Control-Allow-Source-Origin + * This header simply allows the CORS response to contain the AMP-Access-Control-Allow-Source-Origin header. + * + * @link https://www.ampproject.org/docs/fundamentals/amp-cors-requests + * @since 1.0 + */ + public static function send_cors_headers() { + $origin = null; + $source_origin = null; + if ( isset( $_SERVER['HTTP_ORIGIN'] ) ) { + $origin = wp_validate_redirect( wp_sanitize_redirect( esc_url_raw( wp_unslash( $_SERVER['HTTP_ORIGIN'] ) ) ) ); + } + if ( isset( self::$purged_amp_query_vars['__amp_source_origin'] ) ) { + $source_origin = wp_validate_redirect( wp_sanitize_redirect( esc_url_raw( self::$purged_amp_query_vars['__amp_source_origin'] ) ) ); + } + if ( ! $origin ) { + $origin = $source_origin; + } + + if ( $origin ) { + self::send_header( 'Access-Control-Allow-Origin', $origin, array( 'replace' => false ) ); + self::send_header( 'Access-Control-Allow-Credentials', 'true' ); + self::send_header( 'Vary', 'Origin', array( 'replace' => false ) ); + } + if ( $source_origin ) { + self::send_header( 'AMP-Access-Control-Allow-Source-Origin', $source_origin ); + self::send_header( 'Access-Control-Expose-Headers', 'AMP-Access-Control-Allow-Source-Origin', array( 'replace' => false ) ); + } + } + + /** + * Hook into a POST form submissions, such as the comment form or some other form submission. + * + * @since 0.7.0 + * @since 1.0 Moved to AMP_HTTP class. Extracted some logic to send_cors_headers method. + */ + public static function handle_xhr_request() { + $is_amp_xhr = ( + ! empty( self::$purged_amp_query_vars['_wp_amp_action_xhr_converted'] ) + && + ( ! empty( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) + ); + if ( ! $is_amp_xhr ) { + return; + } + + // Intercept POST requests which redirect. + add_filter( 'wp_redirect', array( __CLASS__, 'intercept_post_request_redirect' ), PHP_INT_MAX ); + + // Add special handling for redirecting after comment submission. + add_filter( 'comment_post_redirect', array( __CLASS__, 'filter_comment_post_redirect' ), PHP_INT_MAX, 2 ); + + // Add die handler for AMP error display, most likely due to problem with comment. + add_filter( 'wp_die_handler', function () { + return array( __CLASS__, 'handle_wp_die' ); + } ); + } + + /** + * Intercept the response to a POST request. + * + * @since 0.7.0 + * @since 1.0 Moved to AMP_HTTP class. + * @see wp_redirect() + * + * @param string $location The location to redirect to. + */ + public static function intercept_post_request_redirect( $location ) { + + // Make sure relative redirects get made absolute. + $parsed_location = array_merge( + array( + 'scheme' => 'https', + 'host' => wp_parse_url( home_url(), PHP_URL_HOST ), + 'path' => isset( $_SERVER['REQUEST_URI'] ) ? strtok( wp_unslash( $_SERVER['REQUEST_URI'] ), '?' ) : '/', + ), + wp_parse_url( $location ) + ); + + $absolute_location = ''; + if ( 'https' === $parsed_location['scheme'] ) { + $absolute_location .= $parsed_location['scheme'] . ':'; + } + $absolute_location .= '//' . $parsed_location['host']; + if ( isset( $parsed_location['port'] ) ) { + $absolute_location .= ':' . $parsed_location['port']; + } + $absolute_location .= $parsed_location['path']; + if ( isset( $parsed_location['query'] ) ) { + $absolute_location .= '?' . $parsed_location['query']; + } + if ( isset( $parsed_location['fragment'] ) ) { + $absolute_location .= '#' . $parsed_location['fragment']; + } + + self::send_header( 'AMP-Redirect-To', $absolute_location ); + self::send_header( 'Access-Control-Expose-Headers', 'AMP-Redirect-To', array( 'replace' => false ) ); + + wp_send_json_success(); + } + + /** + * New error handler for AMP form submission. + * + * @since 0.7.0 + * @since 1.0 Moved to AMP_HTTP class. + * @see wp_die() + * + * @param WP_Error|string $error The error to handle. + * @param string|int $title Optional. Error title. If `$message` is a `WP_Error` object, + * error data with the key 'title' may be used to specify the title. + * If `$title` is an integer, then it is treated as the response + * code. Default empty. + * @param string|array|int $args { + * Optional. Arguments to control behavior. If `$args` is an integer, then it is treated + * as the response code. Default empty array. + * + * @type int $response The HTTP response code. Default 200 for Ajax requests, 500 otherwise. + * } + */ + public static function handle_wp_die( $error, $title = '', $args = array() ) { + if ( is_int( $title ) ) { + $status_code = $title; + } elseif ( is_int( $args ) ) { + $status_code = $args; + } elseif ( is_array( $args ) && isset( $args['response'] ) ) { + $status_code = $args['response']; + } else { + $status_code = 500; + } + status_header( $status_code ); + + if ( is_wp_error( $error ) ) { + $error = $error->get_error_message(); + } + + // Message will be shown in template defined by AMP_Theme_Support::amend_comment_form(). + wp_send_json( array( + 'error' => amp_wp_kses_mustache( $error ), + ) ); + } + + /** + * Handle comment_post_redirect to ensure page reload is done when comments_live_list is not supported, while sending back a success message when it is. + * + * @since 0.7.0 + * @since 1.0 Moved to AMP_HTTP class. + * + * @param string $url Comment permalink to redirect to. + * @param WP_Comment $comment Posted comment. + * + * @return string|null URL if redirect to be done; otherwise function will exist. + */ + public static function filter_comment_post_redirect( $url, $comment ) { + $theme_support = AMP_Theme_Support::get_theme_support_args(); + + // Cause a page refresh if amp-live-list is not implemented for comments via add_theme_support( 'amp', array( 'comments_live_list' => true ) ). + if ( empty( $theme_support['comments_live_list'] ) ) { + /* + * Add the comment ID to the URL to force AMP to refresh the page. + * This is ideally a temporary workaround to deal with https://github.com/ampproject/amphtml/issues/14170 + */ + $url = add_query_arg( 'comment', $comment->comment_ID, $url ); + + // Pass URL along to wp_redirect(). + return $url; + } + + // Create a success message to display to the user. + if ( '1' === (string) $comment->comment_approved ) { + $message = __( 'Your comment has been posted.', 'amp' ); + } else { + $message = __( 'Your comment is awaiting moderation.', 'default' ); // Note core string re-use. + } + + /** + * Filters the message when comment submitted success message when + * + * @since 0.7 + */ + $message = apply_filters( 'amp_comment_posted_message', $message, $comment ); + + // Message will be shown in template defined by AMP_Theme_Support::amend_comment_form(). + wp_send_json( array( + 'message' => amp_wp_kses_mustache( $message ), + ) ); + + return null; + } +} diff --git a/includes/class-amp-response-headers.php b/includes/class-amp-response-headers.php deleted file mode 100644 index 8066240a1b0..00000000000 --- a/includes/class-amp-response-headers.php +++ /dev/null @@ -1,90 +0,0 @@ - true, - 'status_code' => null, - ), - $args - ); - - self::$headers_sent[] = array_merge( compact( 'name', 'value' ), $args ); - if ( headers_sent() ) { - return false; - } - - header( - sprintf( '%s: %s', $name, $value ), - $args['replace'], - $args['status_code'] - ); - return true; - } - - /** - * Send Server-Timing header. - * - * If WP_DEBUG is not enabled and an admin user (who can manage_options) is not logged-in, the Server-Header will not be sent. - * - * @since 1.0 - * - * @param string $name Name. - * @param float $duration Duration. If negative, will be added to microtime( true ). Optional. - * @param string $description Description. Optional. - * @return bool Return value of send_header call. If WP_DEBUG is not enabled or admin user (who can manage_options) is not logged-in, this will always return false. - */ - public static function send_server_timing( $name, $duration = null, $description = null ) { - if ( ! WP_DEBUG && ! current_user_can( 'manage_options' ) ) { - return false; - } - $value = $name; - if ( isset( $description ) ) { - $value .= sprintf( ';desc="%s"', str_replace( array( '\\', '"' ), '', substr( $description, 0, 100 ) ) ); - } - if ( isset( $duration ) ) { - if ( $duration < 0 ) { - $duration = microtime( true ) + $duration; - } - $value .= sprintf( ';dur=%f', $duration * 1000 ); - } - return self::send_header( 'Server-Timing', $value, array( 'replace' => false ) ); - } -} diff --git a/includes/class-amp-story-post-type.php b/includes/class-amp-story-post-type.php index 3e5214075c6..9685ff5d933 100644 --- a/includes/class-amp-story-post-type.php +++ b/includes/class-amp-story-post-type.php @@ -126,7 +126,10 @@ public static function filter_kses_allowed_html( $allowed_tags ) { // @todo This perhaps should not be allowed if user does not have capability. foreach ( $allowed_tags as &$allowed_tag ) { - $allowed_tag['grid-area'] = true; + $allowed_tag['grid-area'] = true; + $allowed_tag['animate-in'] = true; + $allowed_tag['animate-in-duration'] = true; + $allowed_tag['animate-in-delay'] = true; } return $allowed_tags; diff --git a/includes/class-amp-theme-support.php b/includes/class-amp-theme-support.php index 2cb96fd7023..5ff1e50f386 100644 --- a/includes/class-amp-theme-support.php +++ b/includes/class-amp-theme-support.php @@ -93,15 +93,6 @@ class AMP_Theme_Support { 'attachment', ); - /** - * AMP-specific query vars that were purged. - * - * @since 0.7 - * @see AMP_Theme_Support::purge_amp_query_vars() - * @var string[] - */ - public static $purged_amp_query_vars = array(); - /** * Start time when init was called. * @@ -139,9 +130,6 @@ public static function init() { self::$init_start_time = microtime( true ); - self::purge_amp_query_vars(); - self::handle_xhr_request(); - require_once AMP__DIR__ . '/includes/amp-post-template-actions.php'; add_action( 'widgets_init', array( __CLASS__, 'register_widgets' ) ); @@ -842,242 +830,6 @@ public static function add_hooks() { // @todo Add character conversion. } - /** - * Remove query vars that come in requests such as for amp-live-list. - * - * WordPress should generally not respond differently to requests when these parameters - * are present. In some cases, when a query param such as __amp_source_origin is present - * then it would normally get included into pagination links generated by get_pagenum_link(). - * The whitelist sanitizer empties out links that contain this string as it matches the - * blacklisted_value_regex. So by preemptively scrubbing any reference to these query vars - * we can ensure that WordPress won't end up referencing them in any way. - * - * @since 0.7 - */ - public static function purge_amp_query_vars() { - $query_vars = array( - '__amp_source_origin', - '_wp_amp_action_xhr_converted', - 'amp_latest_update_time', - 'amp_last_check_time', - ); - - // Scrub input vars. - foreach ( $query_vars as $query_var ) { - if ( ! isset( $_GET[ $query_var ] ) ) { // phpcs:ignore - continue; - } - self::$purged_amp_query_vars[ $query_var ] = wp_unslash( $_GET[ $query_var ] ); // phpcs:ignore - unset( $_REQUEST[ $query_var ], $_GET[ $query_var ] ); - $scrubbed = true; - } - - if ( isset( $scrubbed ) ) { - $build_query = function( $query ) use ( $query_vars ) { - $pattern = '/^(' . join( '|', $query_vars ) . ')(?==|$)/'; - $pairs = array(); - foreach ( explode( '&', $query ) as $pair ) { - if ( ! preg_match( $pattern, $pair ) ) { - $pairs[] = $pair; - } - } - return join( '&', $pairs ); - }; - - // Scrub QUERY_STRING. - if ( ! empty( $_SERVER['QUERY_STRING'] ) ) { - $_SERVER['QUERY_STRING'] = $build_query( $_SERVER['QUERY_STRING'] ); - } - - // Scrub REQUEST_URI. - if ( ! empty( $_SERVER['REQUEST_URI'] ) ) { - list( $path, $query ) = explode( '?', $_SERVER['REQUEST_URI'], 2 ); - - $pairs = $build_query( $query ); - $_SERVER['REQUEST_URI'] = $path; - if ( ! empty( $pairs ) ) { - $_SERVER['REQUEST_URI'] .= "?{$pairs}"; - } - } - } - } - - /** - * Hook into a POST form submissions, such as the comment form or some other form submission. - * - * @since 0.7.0 - */ - public static function handle_xhr_request() { - $is_amp_xhr = ( - ! empty( self::$purged_amp_query_vars['_wp_amp_action_xhr_converted'] ) - && - ! empty( self::$purged_amp_query_vars['__amp_source_origin'] ) - && - ( ! empty( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) - ); - if ( ! $is_amp_xhr ) { - return; - } - - // Send AMP response header. - $origin = wp_validate_redirect( wp_sanitize_redirect( esc_url_raw( self::$purged_amp_query_vars['__amp_source_origin'] ) ) ); - if ( $origin ) { - AMP_Response_Headers::send_header( 'AMP-Access-Control-Allow-Source-Origin', $origin, array( 'replace' => true ) ); - } - - // Intercept POST requests which redirect. - add_filter( 'wp_redirect', array( __CLASS__, 'intercept_post_request_redirect' ), PHP_INT_MAX ); - - // Add special handling for redirecting after comment submission. - add_filter( 'comment_post_redirect', array( __CLASS__, 'filter_comment_post_redirect' ), PHP_INT_MAX, 2 ); - - // Add die handler for AMP error display, most likely due to problem with comment. - add_filter( 'wp_die_handler', function() { - return array( __CLASS__, 'handle_wp_die' ); - } ); - - } - - /** - * Strip tags that are not allowed in amp-mustache. - * - * @since 0.7.0 - * - * @param string $text Text to sanitize. - * @return string Sanitized text. - */ - protected static function wp_kses_amp_mustache( $text ) { - $amp_mustache_allowed_html_tags = array( 'strong', 'b', 'em', 'i', 'u', 's', 'small', 'mark', 'del', 'ins', 'sup', 'sub' ); - return wp_kses( $text, array_fill_keys( $amp_mustache_allowed_html_tags, array() ) ); - } - - /** - * Handle comment_post_redirect to ensure page reload is done when comments_live_list is not supported, while sending back a success message when it is. - * - * @since 0.7.0 - * - * @param string $url Comment permalink to redirect to. - * @param WP_Comment $comment Posted comment. - * @return string|null URL if redirect to be done; otherwise function will exist. - */ - public static function filter_comment_post_redirect( $url, $comment ) { - $theme_support = self::get_theme_support_args(); - - // Cause a page refresh if amp-live-list is not implemented for comments via add_theme_support( 'amp', array( 'comments_live_list' => true ) ). - if ( empty( $theme_support['comments_live_list'] ) ) { - /* - * Add the comment ID to the URL to force AMP to refresh the page. - * This is ideally a temporary workaround to deal with https://github.com/ampproject/amphtml/issues/14170 - */ - $url = add_query_arg( 'comment', $comment->comment_ID, $url ); - - // Pass URL along to wp_redirect(). - return $url; - } - - // Create a success message to display to the user. - if ( '1' === (string) $comment->comment_approved ) { - $message = __( 'Your comment has been posted.', 'amp' ); - } else { - $message = __( 'Your comment is awaiting moderation.', 'default' ); // Note core string re-use. - } - - /** - * Filters the message when comment submitted success message when - * - * @since 0.7 - */ - $message = apply_filters( 'amp_comment_posted_message', $message, $comment ); - - // Message will be shown in template defined by AMP_Theme_Support::amend_comment_form(). - wp_send_json( array( - 'message' => self::wp_kses_amp_mustache( $message ), - ) ); - return null; - } - - /** - * New error handler for AMP form submission. - * - * @since 0.7.0 - * @see wp_die() - * - * @param WP_Error|string $error The error to handle. - * @param string|int $title Optional. Error title. If `$message` is a `WP_Error` object, - * error data with the key 'title' may be used to specify the title. - * If `$title` is an integer, then it is treated as the response - * code. Default empty. - * @param string|array|int $args { - * Optional. Arguments to control behavior. If `$args` is an integer, then it is treated - * as the response code. Default empty array. - * - * @type int $response The HTTP response code. Default 200 for Ajax requests, 500 otherwise. - * } - */ - public static function handle_wp_die( $error, $title = '', $args = array() ) { - if ( is_int( $title ) ) { - $status_code = $title; - } elseif ( is_int( $args ) ) { - $status_code = $args; - } elseif ( is_array( $args ) && isset( $args['response'] ) ) { - $status_code = $args['response']; - } else { - $status_code = 500; - } - status_header( $status_code ); - - if ( is_wp_error( $error ) ) { - $error = $error->get_error_message(); - } - - // Message will be shown in template defined by AMP_Theme_Support::amend_comment_form(). - wp_send_json( array( - 'error' => self::wp_kses_amp_mustache( $error ), - ) ); - } - - /** - * Intercept the response to a POST request. - * - * @since 0.7.0 - * @see wp_redirect() - * - * @param string $location The location to redirect to. - */ - public static function intercept_post_request_redirect( $location ) { - - // Make sure relative redirects get made absolute. - $parsed_location = array_merge( - array( - 'scheme' => 'https', - 'host' => wp_parse_url( home_url(), PHP_URL_HOST ), - 'path' => isset( $_SERVER['REQUEST_URI'] ) ? strtok( wp_unslash( $_SERVER['REQUEST_URI'] ), '?' ) : '/', - ), - wp_parse_url( $location ) - ); - - $absolute_location = ''; - if ( 'https' === $parsed_location['scheme'] ) { - $absolute_location .= $parsed_location['scheme'] . ':'; - } - $absolute_location .= '//' . $parsed_location['host']; - if ( isset( $parsed_location['port'] ) ) { - $absolute_location .= ':' . $parsed_location['port']; - } - $absolute_location .= $parsed_location['path']; - if ( isset( $parsed_location['query'] ) ) { - $absolute_location .= '?' . $parsed_location['query']; - } - if ( isset( $parsed_location['fragment'] ) ) { - $absolute_location .= '#' . $parsed_location['fragment']; - } - - AMP_Response_Headers::send_header( 'AMP-Redirect-To', $absolute_location ); - AMP_Response_Headers::send_header( 'Access-Control-Expose-Headers', 'AMP-Redirect-To' ); - - wp_send_json_success(); - } - /** * Register/override widgets. * @@ -1853,7 +1605,7 @@ public static function prepare_response( $response, $args = array() ) { }; } - AMP_Response_Headers::send_server_timing( 'amp_output_buffer', -self::$init_start_time, 'AMP Output Buffer' ); + AMP_HTTP::send_server_timing( 'amp_output_buffer', -self::$init_start_time, 'AMP Output Buffer' ); $dom_parse_start = microtime( true ); @@ -1903,7 +1655,7 @@ public static function prepare_response( $response, $args = array() ) { $dom->documentElement->setAttribute( 'amp', '' ); } - AMP_Response_Headers::send_server_timing( 'amp_dom_parse', -$dom_parse_start, 'AMP DOM Parse' ); + AMP_HTTP::send_server_timing( 'amp_dom_parse', -$dom_parse_start, 'AMP DOM Parse' ); $assets = AMP_Content_Sanitizer::sanitize_document( $dom, self::$sanitizer_classes, $args ); @@ -1987,7 +1739,7 @@ public static function prepare_response( $response, $args = array() ) { $response = "\n"; $response .= AMP_DOM_Utils::get_content_from_dom_node( $dom, $dom->documentElement ); - AMP_Response_Headers::send_server_timing( 'amp_dom_serialize', -$dom_serialize_start, 'AMP DOM Serialize' ); + AMP_HTTP::send_server_timing( 'amp_dom_serialize', -$dom_serialize_start, 'AMP DOM Serialize' ); // Cache response if enabled. if ( $cache_response ) { diff --git a/includes/options/class-amp-options-menu.php b/includes/options/class-amp-options-menu.php index 327cb9f17d8..854838c2af3 100644 --- a/includes/options/class-amp-options-menu.php +++ b/includes/options/class-amp-options-menu.php @@ -291,15 +291,33 @@ public function render_validation_handling() {

diff --git a/includes/sanitizers/class-amp-allowed-tags-generated.php b/includes/sanitizers/class-amp-allowed-tags-generated.php index 93e1a868212..650da65f819 100644 --- a/includes/sanitizers/class-amp-allowed-tags-generated.php +++ b/includes/sanitizers/class-amp-allowed-tags-generated.php @@ -13,7 +13,7 @@ */ class AMP_Allowed_Tags_Generated { - private static $spec_file_revision = 712; + private static $spec_file_revision = 720; private static $minimum_validator_revision_required = 348; private static $allowed_tags = array( @@ -1133,6 +1133,16 @@ class AMP_Allowed_Tags_Generated { 4, ), ), + 'reference_points' => array( + 'AMP-CAROUSEL lightbox [child]' => array( + 'mandatory' => false, + 'unique' => false, + ), + 'AMP-CAROUSEL lightbox [lightbox-exclude]' => array( + 'mandatory' => false, + 'unique' => false, + ), + ), 'requires_extension' => array( 'amp-carousel', 'amp-lightbox-gallery', @@ -1284,6 +1294,7 @@ class AMP_Allowed_Tags_Generated { ), 'when-ended' => array( 'value_casei' => array( + 'continue', 'stop', ), ), @@ -2340,6 +2351,33 @@ class AMP_Allowed_Tags_Generated { ), ), ), + 'amp-image-slider' => array( + array( + 'attr_spec_list' => array( + 'disable-hint-reappear' => array(), + 'media' => array(), + 'noloading' => array( + 'value' => array( + '', + ), + ), + ), + 'tag_spec' => array( + 'amp_layout' => array( + 'supported_layouts' => array( + 2, + 9, + 1, + 4, + ), + ), + 'requires_extension' => array( + 'amp-image-slider', + ), + 'spec_url' => 'https://www.ampproject.org/docs/reference/components/amp-image-slider', + ), + ), + ), 'amp-img' => array( array( 'attr_spec_list' => array( @@ -2598,6 +2636,7 @@ class AMP_Allowed_Tags_Generated { 9, 1, 4, + 5, ), ), 'spec_url' => 'https://www.ampproject.org/docs/reference/components/amp-layout', @@ -2607,6 +2646,7 @@ class AMP_Allowed_Tags_Generated { 'amp-lightbox' => array( array( 'attr_spec_list' => array( + '[open]' => array(), 'animate-in' => array( 'value_casei' => array( 'fade-in', @@ -2724,6 +2764,20 @@ class AMP_Allowed_Tags_Generated { 3, ), ), + 'reference_points' => array( + 'AMP-LIVE-LIST [items]' => array( + 'mandatory' => true, + 'unique' => true, + ), + 'AMP-LIVE-LIST [pagination]' => array( + 'mandatory' => false, + 'unique' => true, + ), + 'AMP-LIVE-LIST [update]' => array( + 'mandatory' => true, + 'unique' => true, + ), + ), 'requires_extension' => array( 'amp-live-list', ), @@ -2760,6 +2814,16 @@ class AMP_Allowed_Tags_Generated { array( 'attr_spec_list' => array(), 'tag_spec' => array( + 'reference_points' => array( + 'AMP-NEXT-PAGE > [separator]' => array( + 'mandatory' => false, + 'unique' => true, + ), + 'amp-next-page extension .json configuration' => array( + 'mandatory' => true, + 'unique' => true, + ), + ), 'requires_extension' => array( 'amp-next-page', ), @@ -2782,6 +2846,12 @@ class AMP_Allowed_Tags_Generated { ), ), 'tag_spec' => array( + 'reference_points' => array( + 'AMP-NEXT-PAGE > [separator]' => array( + 'mandatory' => false, + 'unique' => true, + ), + ), 'requires_extension' => array( 'amp-next-page', ), @@ -2951,6 +3021,11 @@ class AMP_Allowed_Tags_Generated { 'amp-pan-zoom' => array( array( 'attr_spec_list' => array( + 'disable-double-tap' => array( + 'value' => array( + '', + ), + ), 'initial-scale' => array( 'value_regex' => '[0-9]+(\\.[0-9]+)?', ), @@ -3292,6 +3367,16 @@ class AMP_Allowed_Tags_Generated { 'disallowed_ancestor' => array( 'amp-selector', ), + 'reference_points' => array( + 'AMP-SELECTOR child' => array( + 'mandatory' => false, + 'unique' => false, + ), + 'AMP-SELECTOR option' => array( + 'mandatory' => false, + 'unique' => false, + ), + ), 'requires_extension' => array( 'amp-selector', ), @@ -3661,6 +3746,12 @@ class AMP_Allowed_Tags_Generated { 'attr_spec_list' => array(), 'tag_spec' => array( 'mandatory_ancestor' => 'amp-story-page', + 'reference_points' => array( + 'AMP-STORY-CTA-LAYER animate-in' => array( + 'mandatory' => false, + 'unique' => false, + ), + ), ), ), ), @@ -3679,6 +3770,16 @@ class AMP_Allowed_Tags_Generated { ), 'tag_spec' => array( 'mandatory_ancestor' => 'amp-story-page', + 'reference_points' => array( + 'AMP-STORY-GRID-LAYER animate-in' => array( + 'mandatory' => false, + 'unique' => false, + ), + 'AMP-STORY-GRID-LAYER default' => array( + 'mandatory' => false, + 'unique' => false, + ), + ), ), ), ), @@ -4097,6 +4198,40 @@ class AMP_Allowed_Tags_Generated { ), ), ), + 'amp-viqeo-player' => array( + array( + 'attr_spec_list' => array( + 'autoplay' => array(), + 'data-profileid' => array( + 'mandatory' => true, + 'value_regex' => '[0-9a-f]*', + ), + 'data-videoid' => array( + 'mandatory' => true, + ), + 'media' => array(), + 'noloading' => array( + 'value' => array( + '', + ), + ), + ), + 'tag_spec' => array( + 'amp_layout' => array( + 'supported_layouts' => array( + 6, + 2, + 3, + 7, + 4, + ), + ), + 'requires_extension' => array( + 'amp-viqeo-player', + ), + ), + ), + ), 'amp-vk' => array( array( 'attr_spec_list' => array( @@ -4961,6 +5096,30 @@ class AMP_Allowed_Tags_Generated { 'spec_name' => 'FORM DIV [submit-error][template]', ), ), + array( + 'attr_spec_list' => array( + 'first' => array( + 'mandatory' => true, + ), + ), + 'tag_spec' => array( + 'mandatory_parent' => 'amp-image-slider', + 'spec_name' => 'AMP-IMAGE-SLIDER > DIV [first]', + 'spec_url' => 'https://www.ampproject.org/docs/reference/components/amp-image-slider', + ), + ), + array( + 'attr_spec_list' => array( + 'second' => array( + 'mandatory' => true, + ), + ), + 'tag_spec' => array( + 'mandatory_parent' => 'amp-image-slider', + 'spec_name' => 'AMP-IMAGE-SLIDER > DIV [second]', + 'spec_url' => 'https://www.ampproject.org/docs/reference/components/amp-image-slider', + ), + ), ), 'dl' => array( array( @@ -10082,6 +10241,31 @@ class AMP_Allowed_Tags_Generated { ), ), ), + array( + 'attr_spec_list' => array( + 'async' => array( + 'mandatory' => true, + 'value' => array( + '', + ), + ), + 'nonce' => array(), + 'type' => array( + 'value_casei' => array( + 'text/javascript', + ), + ), + ), + 'tag_spec' => array( + 'extension_spec' => array( + 'name' => 'amp-image-slider', + 'version' => array( + '0.1', + 'latest', + ), + ), + ), + ), array( 'attr_spec_list' => array( 'async' => array( @@ -11247,6 +11431,31 @@ class AMP_Allowed_Tags_Generated { ), ), ), + array( + 'attr_spec_list' => array( + 'async' => array( + 'mandatory' => true, + 'value' => array( + '', + ), + ), + 'nonce' => array(), + 'type' => array( + 'value_casei' => array( + 'text/javascript', + ), + ), + ), + 'tag_spec' => array( + 'extension_spec' => array( + 'name' => 'amp-viqeo-player', + 'version' => array( + '0.1', + 'latest', + ), + ), + ), + ), array( 'attr_spec_list' => array( 'async' => array( @@ -11916,6 +12125,9 @@ class AMP_Allowed_Tags_Generated { 'stroke-miterlimit' => array(), 'stroke-opacity' => array(), 'stroke-width' => array(), + 'style' => array( + 'blacklisted_value_regex' => '!important', + ), 'systemlanguage' => array(), 'text-anchor' => array(), 'text-decoration' => array(), @@ -12230,6 +12442,16 @@ class AMP_Allowed_Tags_Generated { ), 'tag_spec' => array( 'mandatory_parent' => 'amp-story-auto-ads', + 'reference_points' => array( + 'AMP-STORY-GRID-LAYER animate-in' => array( + 'mandatory' => false, + 'unique' => false, + ), + 'AMP-STORY-GRID-LAYER default' => array( + 'mandatory' => false, + 'unique' => false, + ), + ), 'requires_extension' => array( 'amp-mustache', ), @@ -13450,6 +13672,305 @@ class AMP_Allowed_Tags_Generated { ); + private static $reference_points = array( + 'AMP-CAROUSEL lightbox [child]' => array( + 'attr_spec_list' => array( + 'lightbox-thumbnail-id' => array( + 'value_regex_casei' => '^[a-z][a-z\\d_-]*', + ), + ), + 'tag_spec' => array( + 'spec_name' => 'AMP-CAROUSEL lightbox [child]', + ), + ), + 'AMP-CAROUSEL lightbox [lightbox-exclude]' => array( + 'attr_spec_list' => array( + 'lightbox-exclude' => array( + 'mandatory' => true, + ), + ), + 'tag_spec' => array( + 'spec_name' => 'AMP-CAROUSEL lightbox [lightbox-exclude]', + ), + ), + 'AMP-LIVE-LIST [items]' => array( + 'attr_spec_list' => array( + 'items' => array( + 'mandatory' => true, + ), + ), + 'tag_spec' => array( + 'reference_points' => array( + 'AMP-LIVE-LIST [items] item' => array( + 'mandatory' => false, + 'unique' => false, + ), + ), + 'spec_name' => 'AMP-LIVE-LIST [items]', + 'spec_url' => 'https://www.ampproject.org/docs/reference/components/amp-live-list#items', + ), + ), + 'AMP-LIVE-LIST [items] item' => array( + 'attr_spec_list' => array( + 'data-sort-time' => array( + 'mandatory' => true, + ), + 'data-tombstone' => array(), + 'data-update-time' => array(), + 'id' => array( + 'mandatory' => true, + ), + ), + 'tag_spec' => array( + 'spec_name' => 'AMP-LIVE-LIST [items] item', + 'spec_url' => 'https://www.ampproject.org/docs/reference/components/amp-live-list#items', + ), + ), + 'AMP-LIVE-LIST [pagination]' => array( + 'attr_spec_list' => array( + 'pagination' => array( + 'mandatory' => true, + ), + ), + 'tag_spec' => array( + 'spec_name' => 'AMP-LIVE-LIST [pagination]', + 'spec_url' => 'https://www.ampproject.org/docs/reference/components/amp-live-list#pagination', + ), + ), + 'AMP-LIVE-LIST [update]' => array( + 'attr_spec_list' => array( + 'update' => array( + 'mandatory' => true, + ), + ), + 'tag_spec' => array( + 'spec_name' => 'AMP-LIVE-LIST [update]', + 'spec_url' => 'https://www.ampproject.org/docs/reference/components/amp-live-list#update', + ), + ), + 'AMP-NEXT-PAGE > [separator]' => array( + 'attr_spec_list' => array( + 'separator' => array( + 'mandatory' => true, + ), + ), + 'tag_spec' => array( + 'mandatory_parent' => 'amp-next-page', + 'spec_name' => 'AMP-NEXT-PAGE > [separator]', + ), + ), + 'AMP-SELECTOR child' => array( + 'attr_spec_list' => array(), + 'tag_spec' => array( + 'reference_points' => array( + 'AMP-SELECTOR child' => array( + 'mandatory' => false, + 'unique' => false, + ), + 'AMP-SELECTOR option' => array( + 'mandatory' => false, + 'unique' => false, + ), + ), + 'spec_name' => 'AMP-SELECTOR child', + ), + ), + 'AMP-SELECTOR option' => array( + 'attr_spec_list' => array( + 'disabled' => array( + 'value' => array( + '', + ), + ), + 'option' => array( + 'mandatory' => true, + ), + 'selected' => array( + 'value' => array( + '', + ), + ), + ), + 'tag_spec' => array( + 'spec_name' => 'AMP-SELECTOR option', + 'spec_url' => 'https://www.ampproject.org/docs/reference/components/amp-selector', + ), + ), + 'AMP-STORY-CTA-LAYER animate-in' => array( + 'attr_spec_list' => array( + 'animate-in' => array( + 'value' => array( + 'drop', + 'fade-in', + 'fly-in-bottom', + 'fly-in-left', + 'fly-in-right', + 'fly-in-top', + 'pan-down', + 'pan-left', + 'pan-right', + 'pan-up', + 'pulse', + 'rotate-in-left', + 'rotate-in-right', + 'twirl-in', + 'whoosh-in-left', + 'whoosh-in-right', + 'zoom-in', + 'zoom-out', + ), + ), + 'animate-in-after' => array(), + 'animate-in-delay' => array(), + 'animate-in-duration' => array(), + ), + 'tag_spec' => array( + 'reference_points' => array( + 'AMP-STORY-CTA-LAYER animate-in' => array( + 'mandatory' => false, + 'unique' => false, + ), + ), + 'spec_name' => 'AMP-STORY-CTA-LAYER animate-in', + 'spec_url' => 'https://www.ampproject.org/docs/reference/components/amp-story', + ), + ), + 'AMP-STORY-GRID-LAYER animate-in' => array( + 'attr_spec_list' => array( + 'animate-in' => array( + 'value' => array( + 'drop', + 'fade-in', + 'fly-in-bottom', + 'fly-in-left', + 'fly-in-right', + 'fly-in-top', + 'pan-down', + 'pan-left', + 'pan-right', + 'pan-up', + 'pulse', + 'rotate-in-left', + 'rotate-in-right', + 'twirl-in', + 'whoosh-in-left', + 'whoosh-in-right', + 'zoom-in', + 'zoom-out', + ), + ), + 'animate-in-after' => array(), + 'animate-in-delay' => array(), + 'animate-in-duration' => array(), + ), + 'tag_spec' => array( + 'reference_points' => array( + 'AMP-STORY-GRID-LAYER animate-in' => array( + 'mandatory' => false, + 'unique' => false, + ), + ), + 'spec_name' => 'AMP-STORY-GRID-LAYER animate-in', + 'spec_url' => 'https://www.ampproject.org/docs/reference/components/amp-story', + ), + ), + 'AMP-STORY-GRID-LAYER default' => array( + 'attr_spec_list' => array( + 'align-content' => array( + 'value' => array( + 'center', + 'end', + 'space-around', + 'space-between', + 'space-evenly', + 'start', + 'stretch', + ), + ), + 'align-items' => array( + 'value' => array( + 'center', + 'end', + 'start', + 'stretch', + ), + ), + 'align-self' => array( + 'value' => array( + 'center', + 'end', + 'start', + 'stretch', + ), + ), + 'animate-in' => array( + 'value' => array( + 'drop', + 'fade-in', + 'fly-in-bottom', + 'fly-in-left', + 'fly-in-right', + 'fly-in-top', + 'pan-down', + 'pan-left', + 'pan-right', + 'pan-up', + 'pulse', + 'rotate-in-left', + 'rotate-in-right', + 'twirl-in', + 'whoosh-in-left', + 'whoosh-in-right', + 'zoom-in', + 'zoom-out', + ), + ), + 'animate-in-after' => array(), + 'animate-in-delay' => array(), + 'animate-in-duration' => array(), + 'grid-area' => array(), + 'justify-content' => array( + 'value' => array( + 'center', + 'end', + 'space-around', + 'space-between', + 'space-evenly', + 'start', + 'stretch', + ), + ), + 'justify-items' => array( + 'value' => array( + 'center', + 'end', + 'start', + 'stretch', + ), + ), + 'justify-self' => array( + 'value' => array( + 'center', + 'end', + 'start', + 'stretch', + ), + ), + ), + 'tag_spec' => array( + 'reference_points' => array( + 'AMP-STORY-GRID-LAYER animate-in' => array( + 'mandatory' => false, + 'unique' => false, + ), + ), + 'spec_name' => 'AMP-STORY-GRID-LAYER default', + 'spec_url' => 'https://www.ampproject.org/docs/reference/components/amp-story', + ), + ), + ); + + /** * Get allowed tags. * @@ -13476,6 +13997,20 @@ public static function get_allowed_tag( $node_name ) { return null; } + /** + * Get reference point spec. + * + * @since 1.0 + * @param string $tag_spec_name Tag spec name. + * @return array|null Reference point spec, or null if does not exist. + */ + public static function get_reference_point_spec( $tag_spec_name ) { + if ( isset( self::$reference_points[ $tag_spec_name ] ) ) { + return self::$reference_points[ $tag_spec_name ]; + } + return null; + } + /** * Get list of globally-allowed attributes. * diff --git a/includes/sanitizers/class-amp-rule-spec.php b/includes/sanitizers/class-amp-rule-spec.php index b3b61181f21..a09fb94eb5b 100644 --- a/includes/sanitizers/class-amp-rule-spec.php +++ b/includes/sanitizers/class-amp-rule-spec.php @@ -70,39 +70,6 @@ abstract class AMP_Rule_Spec { 9 => 'intrinsic', ); - /** - * If a node type listed here is invalid, it and it's subtree will be - * removed if it is invalid. This is mainly because any children will be - * non-functional without this parent. - * - * If a tag is not listed here, it will be replaced by its children if it - * is invalid. - * - * @todo There are other nodes that should probably be listed here as well. - * - * @var array - */ - public static $node_types_to_remove_if_invalid = array( - 'form', - 'input', - 'link', - 'meta', - 'style', - // Include 'script' here? - ); - - /** - * It is mentioned in the documentation in several places that data-* - * is generally allowed, but there is no specific rule for it in the - * protoascii file, so we include it here. - * - * @var array - */ - public static $whitelisted_attr_regex = array( - '@^data-[a-zA-Z][\\w:.-]*$@uis', - '(update|item|pagination|option|selected|disabled)', // Allowed for live reference points. - ); - /** * List of boolean attributes. * diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 050c24c69e1..4cadd3b017a 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -463,7 +463,7 @@ public function sanitize() { $this->did_convert_elements = true; if ( $this->parse_css_duration > 0.0 ) { - AMP_Response_Headers::send_server_timing( 'amp_parse_css', $this->parse_css_duration, 'AMP Parse CSS' ); + AMP_HTTP::send_server_timing( 'amp_parse_css', $this->parse_css_duration, 'AMP Parse CSS' ); } } diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index b52e7dfcfb9..ec300c31675 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -240,7 +240,7 @@ private function process_alternate_names( $attr_spec_list ) { } /** - * Sanitize the

+

'; + echo ''; + esc_html_e( 'Stale results', 'amp' ); + echo ''; + echo '
'; + if ( ! empty( $staleness['theme'] ) && ! empty( $staleness['plugins'] ) ) { + esc_html_e( 'Different theme and plugins were active when these results were obtained. Please re-check.', 'amp' ); + } elseif ( ! empty( $staleness['theme'] ) ) { + esc_html_e( 'A different theme was active when these results were obtained. Please re-check.', 'amp' ); + } elseif ( ! empty( $staleness['plugins'] ) ) { + esc_html_e( 'Different plugins were active when these results were obtained. Please re-check.', 'amp' ); + } + echo '

'; + } + ?> diff --git a/tests/amp-tag-and-attribute-sanitizer-private-methods-tests.php b/tests/amp-tag-and-attribute-sanitizer-private-methods-tests.php index ecdb081538b..d5027f439e5 100644 --- a/tests/amp-tag-and-attribute-sanitizer-private-methods-tests.php +++ b/tests/amp-tag-and-attribute-sanitizer-private-methods-tests.php @@ -548,7 +548,7 @@ public function test_is_allowed_attribute( $data, $expected ) { } $source = '<' . $data['tag_name'] . ' ' . $attribute . '>Some test content'; - $attr_spec_list = $this->allowed_tags[ $data['tag_name'] ][ $data['rule_spec_index'] ]['attr_spec_list']; + $attr_spec_list = array_merge( $this->globally_allowed_attrs, $this->allowed_tags[ $data['tag_name'] ][ $data['rule_spec_index'] ]['attr_spec_list'] ); if ( isset( $this->allowed_tags[ $data['tag_name'] ][ $data['rule_spec_index'] ]['tag_spec']['amp_layout'] ) ) { $attr_spec_list = array_merge( $attr_spec_list, $this->layout_allowed_attrs ); } @@ -560,11 +560,12 @@ public function test_is_allowed_attribute( $data, $expected ) { } } - $dom = AMP_DOM_Utils::get_dom_from_content( $source ); + $dom = AMP_DOM_Utils::get_dom_from_content( $source ); $sanitizer = new AMP_Tag_And_Attribute_Sanitizer( $dom ); - $node = $dom->getElementsByTagName( $data['tag_name'] )->item( 0 ); + $node = $dom->getElementsByTagName( $data['tag_name'] )->item( 0 ); + $attr = $node->getAttributeNode( $data['attribute_name'] ); - $got = $this->invoke_method( $sanitizer, $data['func_name'], array( $data['attribute_name'], $attr_spec_list ) ); + $got = $this->invoke_method( $sanitizer, $data['func_name'], array( $attr, $attr_spec_list ) ); $this->assertEquals( $expected, $got, sprintf( "using source: %s\n%s", $source, wp_json_encode( $data ) ) ); } diff --git a/tests/test-class-amp-http.php b/tests/test-class-amp-http.php new file mode 100644 index 00000000000..52846b72d6a --- /dev/null +++ b/tests/test-class-amp-http.php @@ -0,0 +1,514 @@ +assertContains( + array( + 'name' => 'Foo', + 'value' => 'Bar', + 'replace' => true, + 'status_code' => null, + ), + AMP_HTTP::$headers_sent + ); + } + + /** + * Test \AMP_HTTP::send_header() when replace arg is passed. + * + * @covers \AMP_HTTP::send_header() + */ + public function test_send_header_replace_arg() { + AMP_HTTP::send_header( 'Foo', 'Bar', array( + 'replace' => false, + ) ); + $this->assertContains( + array( + 'name' => 'Foo', + 'value' => 'Bar', + 'replace' => false, + 'status_code' => null, + ), + AMP_HTTP::$headers_sent + ); + } + + /** + * Test \AMP_HTTP::send_header() when status code is passed. + * + * @covers \AMP_HTTP::send_header() + */ + public function test_send_header_status_code() { + AMP_HTTP::send_header( 'Foo', 'Bar', array( + 'status_code' => 400, + ) ); + $this->assertContains( + array( + 'name' => 'Foo', + 'value' => 'Bar', + 'replace' => true, + 'status_code' => 400, + ), + AMP_HTTP::$headers_sent + ); + } + + /** + * Test \AMP_HTTP::send_server_timing() when positive duration passed. + * + * @covers \AMP_HTTP::send_server_timing() + */ + public function test_send_server_timing_positive_duration() { + AMP_HTTP::send_server_timing( 'name', 123, 'Description' ); + $this->assertCount( 1, AMP_HTTP::$headers_sent ); + $this->assertEquals( 'Server-Timing', AMP_HTTP::$headers_sent[0]['name'] ); + $values = preg_split( '/\s*;\s*/', AMP_HTTP::$headers_sent[0]['value'] ); + $this->assertEquals( 'name', $values[0] ); + $this->assertEquals( 'desc="Description"', $values[1] ); + $this->assertStringStartsWith( 'dur=123000.', $values[2] ); + $this->assertFalse( AMP_HTTP::$headers_sent[0]['replace'] ); + $this->assertNull( AMP_HTTP::$headers_sent[0]['status_code'] ); + } + + /** + * Test \AMP_HTTP::send_server_timing() when positive duration passed. + * + * @covers \AMP_HTTP::send_server_timing() + */ + public function test_send_server_timing_negative_duration() { + AMP_HTTP::send_server_timing( 'name', -microtime( true ) ); + $this->assertCount( 1, AMP_HTTP::$headers_sent ); + $this->assertEquals( 'Server-Timing', AMP_HTTP::$headers_sent[0]['name'] ); + $values = preg_split( '/\s*;\s*/', AMP_HTTP::$headers_sent[0]['value'] ); + $this->assertEquals( 'name', $values[0] ); + $this->assertStringStartsWith( 'dur=0.', $values[1] ); + $this->assertFalse( AMP_HTTP::$headers_sent[0]['replace'] ); + $this->assertNull( AMP_HTTP::$headers_sent[0]['status_code'] ); + } + + /** + * Test purge_amp_query_vars. + * + * @covers AMP_HTTP::purge_amp_query_vars() + */ + public function test_purge_amp_query_vars() { + // phpcs:disable WordPress.CSRF.NonceVerification.NoNonceVerification + $bad_query_vars = array( + 'amp_latest_update_time' => '1517199956', + 'amp_last_check_time' => '1517599126', + '__amp_source_origin' => home_url(), + ); + $ok_query_vars = array( + 'bar' => 'baz', + ); + $all_query_vars = array_merge( $bad_query_vars, $ok_query_vars ); + + $_SERVER['QUERY_STRING'] = build_query( $all_query_vars ); + + remove_action( 'wp', 'amp_maybe_add_actions' ); + $this->go_to( add_query_arg( $all_query_vars, home_url( '/foo/' ) ) ); + $_REQUEST = $_GET; + foreach ( $all_query_vars as $key => $value ) { + $this->assertArrayHasKey( $key, $_GET ); + $this->assertArrayHasKey( $key, $_REQUEST ); + $this->assertContains( "$key=$value", $_SERVER['QUERY_STRING'] ); + $this->assertContains( "$key=$value", $_SERVER['REQUEST_URI'] ); + } + + AMP_HTTP::$purged_amp_query_vars = array(); + AMP_HTTP::purge_amp_query_vars(); + $this->assertEqualSets( AMP_HTTP::$purged_amp_query_vars, $bad_query_vars ); + + foreach ( $bad_query_vars as $key => $value ) { + $this->assertArrayNotHasKey( $key, $_GET ); + $this->assertArrayNotHasKey( $key, $_REQUEST ); + $this->assertNotContains( "$key=$value", $_SERVER['QUERY_STRING'] ); + $this->assertNotContains( "$key=$value", $_SERVER['REQUEST_URI'] ); + } + foreach ( $ok_query_vars as $key => $value ) { + $this->assertArrayHasKey( $key, $_GET ); + $this->assertArrayHasKey( $key, $_REQUEST ); + $this->assertContains( "$key=$value", $_SERVER['QUERY_STRING'] ); + $this->assertContains( "$key=$value", $_SERVER['REQUEST_URI'] ); + } + // phpcs:enable WordPress.CSRF.NonceVerification.NoNonceVerification + } + + /** + * Test send_cors_headers(). + * + * @covers AMP_HTTP::send_cors_headers() + */ + public function test_send_cors_headers() { + + // Initial case case. + AMP_HTTP::$headers_sent = array(); + AMP_HTTP::send_cors_headers(); + $this->assertEmpty( AMP_HTTP::$headers_sent ); + + // Try an invalid Origin header. + AMP_HTTP::$headers_sent = array(); + AMP_HTTP::$purged_amp_query_vars = array(); + $_SERVER['HTTP_ORIGIN'] = 'https://evil.example.com'; + AMP_HTTP::send_cors_headers(); + $this->assertEmpty( AMP_HTTP::$headers_sent ); + + // Try an invalid __amp_source_origin. + AMP_HTTP::$headers_sent = array(); + AMP_HTTP::$purged_amp_query_vars = array(); + unset( $_SERVER['HTTP_ORIGIN'] ); + $_GET['__amp_source_origin'] = 'https://evil.example.com'; + AMP_HTTP::send_cors_headers(); + $this->assertEmpty( AMP_HTTP::$headers_sent ); + + // Try an allowed Origin header. + AMP_HTTP::$headers_sent = array(); + AMP_HTTP::$purged_amp_query_vars = array(); + $_SERVER['HTTP_ORIGIN'] = home_url(); + AMP_HTTP::send_cors_headers(); + $this->assertEquals( + array( + array( + 'name' => 'Access-Control-Allow-Origin', + 'value' => home_url(), + 'replace' => false, + 'status_code' => null, + ), + array( + 'name' => 'Access-Control-Allow-Credentials', + 'value' => 'true', + 'replace' => true, + 'status_code' => null, + ), + array( + 'name' => 'Vary', + 'value' => 'Origin', + 'replace' => false, + 'status_code' => null, + ), + ), + AMP_HTTP::$headers_sent + ); + + // The __amp_source_origin is specified but the Origin header is not. + AMP_HTTP::$headers_sent = array(); + $_GET['__amp_source_origin'] = 'https://cdn.ampproject.org'; + $_SERVER['REQUEST_METHOD'] = 'POST'; + unset( $_SERVER['HTTP_ORIGIN'] ); + AMP_HTTP::purge_amp_query_vars(); + AMP_HTTP::send_cors_headers(); + $this->assertEquals( + array( + array( + 'name' => 'Access-Control-Allow-Origin', + 'value' => 'https://cdn.ampproject.org', + 'replace' => false, + 'status_code' => null, + ), + array( + 'name' => 'Access-Control-Allow-Credentials', + 'value' => 'true', + 'replace' => true, + 'status_code' => null, + ), + array( + 'name' => 'Vary', + 'value' => 'Origin', + 'replace' => false, + 'status_code' => null, + ), + array( + 'name' => 'AMP-Access-Control-Allow-Source-Origin', + 'value' => 'https://cdn.ampproject.org', + 'replace' => true, + 'status_code' => null, + ), + array( + 'name' => 'Access-Control-Expose-Headers', + 'value' => 'AMP-Access-Control-Allow-Source-Origin', + 'replace' => false, + 'status_code' => null, + ), + ), + AMP_HTTP::$headers_sent + ); + + // The Origin header and the __amp_source_origin are both specified. + AMP_HTTP::$headers_sent = array(); + $_GET['__amp_source_origin'] = home_url(); + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_SERVER['HTTP_ORIGIN'] = 'https://cdn.ampproject.org'; + AMP_HTTP::purge_amp_query_vars(); + AMP_HTTP::send_cors_headers(); + $this->assertEquals( + array( + array( + 'name' => 'Access-Control-Allow-Origin', + 'value' => 'https://cdn.ampproject.org', + 'replace' => false, + 'status_code' => null, + ), + array( + 'name' => 'Access-Control-Allow-Credentials', + 'value' => 'true', + 'replace' => true, + 'status_code' => null, + ), + array( + 'name' => 'Vary', + 'value' => 'Origin', + 'replace' => false, + 'status_code' => null, + ), + array( + 'name' => 'AMP-Access-Control-Allow-Source-Origin', + 'value' => home_url(), + 'replace' => true, + 'status_code' => null, + ), + array( + 'name' => 'Access-Control-Expose-Headers', + 'value' => 'AMP-Access-Control-Allow-Source-Origin', + 'replace' => false, + 'status_code' => null, + ), + ), + AMP_HTTP::$headers_sent + ); + } + + /** + * Test handle_xhr_request(). + * + * @covers AMP_HTTP::handle_xhr_request() + */ + public function test_handle_xhr_request() { + $_GET['_wp_amp_action_xhr_converted'] = 1; + $_SERVER['REQUEST_METHOD'] = 'POST'; + AMP_HTTP::purge_amp_query_vars(); + AMP_HTTP::handle_xhr_request(); + $this->assertEquals( PHP_INT_MAX, has_filter( 'wp_redirect', array( 'AMP_HTTP', 'intercept_post_request_redirect' ) ) ); + $this->assertEquals( PHP_INT_MAX, has_filter( 'comment_post_redirect', array( 'AMP_HTTP', 'filter_comment_post_redirect' ) ) ); + $this->assertEquals( + array( 'AMP_HTTP', 'handle_wp_die' ), + apply_filters( 'wp_die_handler', '__return_true' ) + ); + } + + /** + * Test intercept_post_request_redirect(). + * + * @covers AMP_HTTP::intercept_post_request_redirect() + */ + public function test_intercept_post_request_redirect() { + + add_theme_support( 'amp' ); + $url = home_url( '', 'https' ) . ':443/?test=true#test'; + + add_filter( 'wp_doing_ajax', '__return_true' ); + add_filter( 'wp_die_ajax_handler', function () { + return '__return_false'; + } ); + + // Test redirecting to full URL with HTTPS protocol. + AMP_HTTP::$headers_sent = array(); + ob_start(); + AMP_HTTP::intercept_post_request_redirect( $url ); + $this->assertEquals( '{"success":true}', ob_get_clean() ); + $this->assertContains( + array( + 'name' => 'AMP-Redirect-To', + 'value' => $url, + 'replace' => true, + 'status_code' => null, + ), + AMP_HTTP::$headers_sent + ); + $this->assertContains( + array( + 'name' => 'Access-Control-Expose-Headers', + 'value' => 'AMP-Redirect-To', + 'replace' => false, + 'status_code' => null, + ), + AMP_HTTP::$headers_sent + ); + + // Test redirecting to non-HTTPS URL. + AMP_HTTP::$headers_sent = array(); + ob_start(); + $url = home_url( '/', 'http' ); + AMP_HTTP::intercept_post_request_redirect( $url ); + $this->assertEquals( '{"success":true}', ob_get_clean() ); + $this->assertContains( + array( + 'name' => 'AMP-Redirect-To', + 'value' => preg_replace( '#^\w+:#', '', $url ), + 'replace' => true, + 'status_code' => null, + ), + AMP_HTTP::$headers_sent + ); + $this->assertContains( + array( + 'name' => 'Access-Control-Expose-Headers', + 'value' => 'AMP-Redirect-To', + 'replace' => false, + 'status_code' => null, + ), + AMP_HTTP::$headers_sent + ); + + // Test redirecting to host-less location. + AMP_HTTP::$headers_sent = array(); + ob_start(); + AMP_HTTP::intercept_post_request_redirect( '/new-location/' ); + $this->assertEquals( '{"success":true}', ob_get_clean() ); + $this->assertContains( + array( + 'name' => 'AMP-Redirect-To', + 'value' => set_url_scheme( home_url( '/new-location/' ), 'https' ), + 'replace' => true, + 'status_code' => null, + ), + AMP_HTTP::$headers_sent + ); + + // Test redirecting to scheme-less location. + AMP_HTTP::$headers_sent = array(); + ob_start(); + $url = home_url( '/new-location/' ); + AMP_HTTP::intercept_post_request_redirect( substr( $url, strpos( $url, ':' ) + 1 ) ); + $this->assertEquals( '{"success":true}', ob_get_clean() ); + $this->assertContains( + array( + 'name' => 'AMP-Redirect-To', + 'value' => set_url_scheme( home_url( '/new-location/' ), 'https' ), + 'replace' => true, + 'status_code' => null, + ), + AMP_HTTP::$headers_sent + ); + + // Test redirecting to empty location. + AMP_HTTP::$headers_sent = array(); + ob_start(); + AMP_HTTP::intercept_post_request_redirect( '' ); + $this->assertEquals( '{"success":true}', ob_get_clean() ); + $this->assertContains( + array( + 'name' => 'AMP-Redirect-To', + 'value' => set_url_scheme( home_url(), 'https' ), + 'replace' => true, + 'status_code' => null, + ), + AMP_HTTP::$headers_sent + ); + } + + /** + * Test handle_wp_die(). + * + * @covers AMP_HTTP::handle_wp_die() + */ + public function test_handle_wp_die() { + add_filter( 'wp_doing_ajax', '__return_true' ); + add_filter( 'wp_die_ajax_handler', function() { + return '__return_null'; + } ); + + ob_start(); + AMP_HTTP::handle_wp_die( 'string' ); + $this->assertEquals( '{"error":"string"}', ob_get_clean() ); + + ob_start(); + $error = new WP_Error( 'code', 'The Message' ); + AMP_HTTP::handle_wp_die( $error ); + $this->assertEquals( '{"error":"The Message"}', ob_get_clean() ); + } + + /** + * Test filter_comment_post_redirect(). + * + * @covers AMP_HTTP::filter_comment_post_redirect() + */ + public function test_filter_comment_post_redirect() { + add_filter( 'wp_doing_ajax', '__return_true' ); + add_filter( 'wp_die_ajax_handler', function() { + return '__return_null'; + } ); + + add_theme_support( 'amp' ); + $post = $this->factory()->post->create_and_get(); + $comment = $this->factory()->comment->create_and_get( array( + 'comment_post_ID' => $post->ID, + ) ); + $url = get_comment_link( $comment ); + + // Test without comments_live_list. + $filtered_url = AMP_HTTP::filter_comment_post_redirect( $url, $comment ); + $this->assertNotEquals( + strtok( $url, '#' ), + strtok( $filtered_url, '#' ) + ); + + // Test with comments_live_list. + add_theme_support( 'amp', array( + 'comments_live_list' => true, + ) ); + add_filter( 'amp_comment_posted_message', function( $message, WP_Comment $filter_comment ) { + return sprintf( '(comment=%d,approved=%d)', $filter_comment->comment_ID, $filter_comment->comment_approved ); + }, 10, 2 ); + + // Test approved comment. + $comment->comment_approved = '1'; + ob_start(); + AMP_HTTP::filter_comment_post_redirect( $url, $comment ); + $response = json_decode( ob_get_clean(), true ); + $this->assertArrayHasKey( 'message', $response ); + $this->assertEquals( + sprintf( '(comment=%d,approved=1)', $comment->comment_ID ), + $response['message'] + ); + + // Test moderated comment. + $comment->comment_approved = '0'; + ob_start(); + AMP_HTTP::filter_comment_post_redirect( $url, $comment ); + $response = json_decode( ob_get_clean(), true ); + $this->assertArrayHasKey( 'message', $response ); + $this->assertEquals( + sprintf( '(comment=%d,approved=0)', $comment->comment_ID ), + $response['message'] + ); + } +} diff --git a/tests/test-class-amp-response-headers.php b/tests/test-class-amp-response-headers.php deleted file mode 100644 index a6408dd10e2..00000000000 --- a/tests/test-class-amp-response-headers.php +++ /dev/null @@ -1,116 +0,0 @@ -assertContains( - array( - 'name' => 'Foo', - 'value' => 'Bar', - 'replace' => true, - 'status_code' => null, - ), - AMP_Response_Headers::$headers_sent - ); - } - - /** - * Test \AMP_Response_Headers::send_header() when replace arg is passed. - * - * @covers \AMP_Response_Headers::send_header() - */ - public function test_send_header_replace_arg() { - AMP_Response_Headers::send_header( 'Foo', 'Bar', array( - 'replace' => false, - ) ); - $this->assertContains( - array( - 'name' => 'Foo', - 'value' => 'Bar', - 'replace' => false, - 'status_code' => null, - ), - AMP_Response_Headers::$headers_sent - ); - } - - /** - * Test \AMP_Response_Headers::send_header() when status code is passed. - * - * @covers \AMP_Response_Headers::send_header() - */ - public function test_send_header_status_code() { - AMP_Response_Headers::send_header( 'Foo', 'Bar', array( - 'status_code' => 400, - ) ); - $this->assertContains( - array( - 'name' => 'Foo', - 'value' => 'Bar', - 'replace' => true, - 'status_code' => 400, - ), - AMP_Response_Headers::$headers_sent - ); - } - - /** - * Test \AMP_Response_Headers::send_server_timing() when positive duration passed. - * - * @covers \AMP_Response_Headers::send_server_timing() - */ - public function test_send_server_timing_positive_duration() { - AMP_Response_Headers::send_server_timing( 'name', 123, 'Description' ); - $this->assertCount( 1, AMP_Response_Headers::$headers_sent ); - $this->assertEquals( 'Server-Timing', AMP_Response_Headers::$headers_sent[0]['name'] ); - $values = preg_split( '/\s*;\s*/', AMP_Response_Headers::$headers_sent[0]['value'] ); - $this->assertEquals( 'name', $values[0] ); - $this->assertEquals( 'desc="Description"', $values[1] ); - $this->assertStringStartsWith( 'dur=123000.', $values[2] ); - $this->assertFalse( AMP_Response_Headers::$headers_sent[0]['replace'] ); - $this->assertNull( AMP_Response_Headers::$headers_sent[0]['status_code'] ); - } - - /** - * Test \AMP_Response_Headers::send_server_timing() when positive duration passed. - * - * @covers \AMP_Response_Headers::send_server_timing() - */ - public function test_send_server_timing_negative_duration() { - AMP_Response_Headers::send_server_timing( 'name', -microtime( true ) ); - $this->assertCount( 1, AMP_Response_Headers::$headers_sent ); - $this->assertEquals( 'Server-Timing', AMP_Response_Headers::$headers_sent[0]['name'] ); - $values = preg_split( '/\s*;\s*/', AMP_Response_Headers::$headers_sent[0]['value'] ); - $this->assertEquals( 'name', $values[0] ); - $this->assertStringStartsWith( 'dur=0.', $values[1] ); - $this->assertFalse( AMP_Response_Headers::$headers_sent[0]['replace'] ); - $this->assertNull( AMP_Response_Headers::$headers_sent[0]['status_code'] ); - } -} diff --git a/tests/test-class-amp-theme-support.php b/tests/test-class-amp-theme-support.php index 13f9c1425b6..a1046a1fa20 100644 --- a/tests/test-class-amp-theme-support.php +++ b/tests/test-class-amp-theme-support.php @@ -50,7 +50,7 @@ public function tearDown() { if ( isset( $GLOBALS['wp_customize'] ) ) { $GLOBALS['wp_customize']->stop_previewing_theme(); } - AMP_Response_Headers::$headers_sent = array(); + AMP_HTTP::$headers_sent = array(); } /** @@ -62,16 +62,13 @@ public function test_init() { $_REQUEST['__amp_source_origin'] = 'foo'; $_GET['__amp_source_origin'] = 'foo'; AMP_Theme_Support::init(); - $this->assertNotEquals( 10, has_action( 'widgets_init', array( self::TESTED_CLASS, 'register_widgets' ) ) ); - - // Ensure that purge_amp_query_vars() didn't execute. - $this->assertTrue( isset( $_REQUEST['__amp_source_origin'] ) ); // WPCS: CSRF ok. + $this->assertFalse( has_action( 'widgets_init', array( self::TESTED_CLASS, 'register_widgets' ) ) ); + $this->assertFalse( has_action( 'wp', array( self::TESTED_CLASS, 'finish_init' ) ) ); add_theme_support( 'amp' ); AMP_Theme_Support::init(); $this->assertEquals( 10, has_action( 'widgets_init', array( self::TESTED_CLASS, 'register_widgets' ) ) ); $this->assertEquals( PHP_INT_MAX, has_action( 'wp', array( self::TESTED_CLASS, 'finish_init' ) ) ); - $this->assertFalse( isset( $_REQUEST['__amp_source_origin'] ) ); // WPCS: CSRF ok. } /** @@ -736,284 +733,6 @@ public function test_add_hooks_no_admin_bar() { $this->assertEquals( 100, has_filter( 'show_admin_bar', '__return_false' ) ); } - /** - * Test purge_amp_query_vars. - * - * @covers AMP_Theme_Support::purge_amp_query_vars() - */ - public function test_purge_amp_query_vars() { - // phpcs:disable WordPress.CSRF.NonceVerification.NoNonceVerification - $bad_query_vars = array( - 'amp_latest_update_time' => '1517199956', - 'amp_last_check_time' => '1517599126', - '__amp_source_origin' => home_url(), - ); - $ok_query_vars = array( - 'bar' => 'baz', - ); - $all_query_vars = array_merge( $bad_query_vars, $ok_query_vars ); - - $_SERVER['QUERY_STRING'] = build_query( $all_query_vars ); - - remove_action( 'wp', 'amp_maybe_add_actions' ); - $this->go_to( add_query_arg( $all_query_vars, home_url( '/foo/' ) ) ); - $_REQUEST = $_GET; - foreach ( $all_query_vars as $key => $value ) { - $this->assertArrayHasKey( $key, $_GET ); - $this->assertArrayHasKey( $key, $_REQUEST ); - $this->assertContains( "$key=$value", $_SERVER['QUERY_STRING'] ); - $this->assertContains( "$key=$value", $_SERVER['REQUEST_URI'] ); - } - - AMP_Theme_Support::$purged_amp_query_vars = array(); - AMP_Theme_Support::purge_amp_query_vars(); - $this->assertEqualSets( AMP_Theme_Support::$purged_amp_query_vars, $bad_query_vars ); - - foreach ( $bad_query_vars as $key => $value ) { - $this->assertArrayNotHasKey( $key, $_GET ); - $this->assertArrayNotHasKey( $key, $_REQUEST ); - $this->assertNotContains( "$key=$value", $_SERVER['QUERY_STRING'] ); - $this->assertNotContains( "$key=$value", $_SERVER['REQUEST_URI'] ); - } - foreach ( $ok_query_vars as $key => $value ) { - $this->assertArrayHasKey( $key, $_GET ); - $this->assertArrayHasKey( $key, $_REQUEST ); - $this->assertContains( "$key=$value", $_SERVER['QUERY_STRING'] ); - $this->assertContains( "$key=$value", $_SERVER['REQUEST_URI'] ); - } - // phpcs:enable WordPress.CSRF.NonceVerification.NoNonceVerification - } - - /** - * Test handle_xhr_request(). - * - * @covers AMP_Theme_Support::handle_xhr_request() - */ - public function test_handle_xhr_request() { - AMP_Theme_Support::purge_amp_query_vars(); - AMP_Theme_Support::handle_xhr_request(); - $this->assertEmpty( AMP_Response_Headers::$headers_sent ); - - $_GET['_wp_amp_action_xhr_converted'] = '1'; - - // Try bad source origin. - $_GET['__amp_source_origin'] = 'http://evil.example.com/'; - $_SERVER['REQUEST_METHOD'] = 'POST'; - AMP_Theme_Support::purge_amp_query_vars(); - AMP_Theme_Support::handle_xhr_request(); - $this->assertEmpty( AMP_Response_Headers::$headers_sent ); - - // Try home source origin. - $_GET['__amp_source_origin'] = home_url(); - $_SERVER['REQUEST_METHOD'] = 'POST'; - AMP_Theme_Support::purge_amp_query_vars(); - AMP_Theme_Support::handle_xhr_request(); - $this->assertCount( 1, AMP_Response_Headers::$headers_sent ); - $this->assertEquals( - array( - 'name' => 'AMP-Access-Control-Allow-Source-Origin', - 'value' => home_url(), - 'replace' => true, - 'status_code' => null, - ), - AMP_Response_Headers::$headers_sent[0] - ); - $this->assertEquals( PHP_INT_MAX, has_filter( 'wp_redirect', array( 'AMP_Theme_Support', 'intercept_post_request_redirect' ) ) ); - $this->assertEquals( PHP_INT_MAX, has_filter( 'comment_post_redirect', array( 'AMP_Theme_Support', 'filter_comment_post_redirect' ) ) ); - $this->assertEquals( - array( 'AMP_Theme_Support', 'handle_wp_die' ), - apply_filters( 'wp_die_handler', '__return_true' ) - ); - } - - /** - * Test filter_comment_post_redirect(). - * - * @covers AMP_Theme_Support::filter_comment_post_redirect() - */ - public function test_filter_comment_post_redirect() { - add_filter( 'wp_doing_ajax', '__return_true' ); - add_filter( 'wp_die_ajax_handler', function() { - return '__return_null'; - } ); - - add_theme_support( 'amp' ); - $post = $this->factory()->post->create_and_get(); - $comment = $this->factory()->comment->create_and_get( array( - 'comment_post_ID' => $post->ID, - ) ); - $url = get_comment_link( $comment ); - - // Test without comments_live_list. - $filtered_url = AMP_Theme_Support::filter_comment_post_redirect( $url, $comment ); - $this->assertNotEquals( - strtok( $url, '#' ), - strtok( $filtered_url, '#' ) - ); - - // Test with comments_live_list. - add_theme_support( 'amp', array( - 'comments_live_list' => true, - ) ); - add_filter( 'amp_comment_posted_message', function( $message, WP_Comment $filter_comment ) { - return sprintf( '(comment=%d,approved=%d)', $filter_comment->comment_ID, $filter_comment->comment_approved ); - }, 10, 2 ); - - // Test approved comment. - $comment->comment_approved = '1'; - ob_start(); - AMP_Theme_Support::filter_comment_post_redirect( $url, $comment ); - $response = json_decode( ob_get_clean(), true ); - $this->assertArrayHasKey( 'message', $response ); - $this->assertEquals( - sprintf( '(comment=%d,approved=1)', $comment->comment_ID ), - $response['message'] - ); - - // Test moderated comment. - $comment->comment_approved = '0'; - ob_start(); - AMP_Theme_Support::filter_comment_post_redirect( $url, $comment ); - $response = json_decode( ob_get_clean(), true ); - $this->assertArrayHasKey( 'message', $response ); - $this->assertEquals( - sprintf( '(comment=%d,approved=0)', $comment->comment_ID ), - $response['message'] - ); - } - - /** - * Test handle_wp_die(). - * - * @covers AMP_Theme_Support::handle_wp_die() - */ - public function test_handle_wp_die() { - add_filter( 'wp_doing_ajax', '__return_true' ); - add_filter( 'wp_die_ajax_handler', function() { - return '__return_null'; - } ); - - ob_start(); - AMP_Theme_Support::handle_wp_die( 'string' ); - $this->assertEquals( '{"error":"string"}', ob_get_clean() ); - - ob_start(); - $error = new WP_Error( 'code', 'The Message' ); - AMP_Theme_Support::handle_wp_die( $error ); - $this->assertEquals( '{"error":"The Message"}', ob_get_clean() ); - } - - /** - * Test intercept_post_request_redirect(). - * - * @covers AMP_Theme_Support::intercept_post_request_redirect() - */ - public function test_intercept_post_request_redirect() { - - add_theme_support( 'amp' ); - $url = home_url( '', 'https' ) . ':443/?test=true#test'; - - add_filter( 'wp_doing_ajax', '__return_true' ); - add_filter( 'wp_die_ajax_handler', function () { - return '__return_false'; - } ); - - // Test redirecting to full URL with HTTPS protocol. - AMP_Response_Headers::$headers_sent = array(); - ob_start(); - AMP_Theme_Support::intercept_post_request_redirect( $url ); - $this->assertEquals( '{"success":true}', ob_get_clean() ); - $this->assertContains( - array( - 'name' => 'AMP-Redirect-To', - 'value' => $url, - 'replace' => true, - 'status_code' => null, - ), - AMP_Response_Headers::$headers_sent - ); - $this->assertContains( - array( - 'name' => 'Access-Control-Expose-Headers', - 'value' => 'AMP-Redirect-To', - 'replace' => true, - 'status_code' => null, - ), - AMP_Response_Headers::$headers_sent - ); - - // Test redirecting to non-HTTPS URL. - AMP_Response_Headers::$headers_sent = array(); - ob_start(); - $url = home_url( '/', 'http' ); - AMP_Theme_Support::intercept_post_request_redirect( $url ); - $this->assertEquals( '{"success":true}', ob_get_clean() ); - $this->assertContains( - array( - 'name' => 'AMP-Redirect-To', - 'value' => preg_replace( '#^\w+:#', '', $url ), - 'replace' => true, - 'status_code' => null, - ), - AMP_Response_Headers::$headers_sent - ); - $this->assertContains( - array( - 'name' => 'Access-Control-Expose-Headers', - 'value' => 'AMP-Redirect-To', - 'replace' => true, - 'status_code' => null, - ), - AMP_Response_Headers::$headers_sent - ); - - // Test redirecting to host-less location. - AMP_Response_Headers::$headers_sent = array(); - ob_start(); - AMP_Theme_Support::intercept_post_request_redirect( '/new-location/' ); - $this->assertEquals( '{"success":true}', ob_get_clean() ); - $this->assertContains( - array( - 'name' => 'AMP-Redirect-To', - 'value' => set_url_scheme( home_url( '/new-location/' ), 'https' ), - 'replace' => true, - 'status_code' => null, - ), - AMP_Response_Headers::$headers_sent - ); - - // Test redirecting to scheme-less location. - AMP_Response_Headers::$headers_sent = array(); - ob_start(); - $url = home_url( '/new-location/' ); - AMP_Theme_Support::intercept_post_request_redirect( substr( $url, strpos( $url, ':' ) + 1 ) ); - $this->assertEquals( '{"success":true}', ob_get_clean() ); - $this->assertContains( - array( - 'name' => 'AMP-Redirect-To', - 'value' => set_url_scheme( home_url( '/new-location/' ), 'https' ), - 'replace' => true, - 'status_code' => null, - ), - AMP_Response_Headers::$headers_sent - ); - - // Test redirecting to empty location. - AMP_Response_Headers::$headers_sent = array(); - ob_start(); - AMP_Theme_Support::intercept_post_request_redirect( '' ); - $this->assertEquals( '{"success":true}', ob_get_clean() ); - $this->assertContains( - array( - 'name' => 'AMP-Redirect-To', - 'value' => set_url_scheme( home_url(), 'https' ), - 'replace' => true, - 'status_code' => null, - ), - AMP_Response_Headers::$headers_sent - ); - } - /** * Test register_widgets(). * @@ -1531,7 +1250,7 @@ public function test_prepare_response() { ); $call_prepare_response = function() use ( $original_html, &$prepare_response_args ) { - AMP_Response_Headers::$headers_sent = array(); + AMP_HTTP::$headers_sent = array(); AMP_Validation_Manager::$validation_results = array(); return AMP_Theme_Support::prepare_response( $original_html, $prepare_response_args ); }; @@ -1587,7 +1306,7 @@ public function test_post_processor_cache_effectiveness() { // Simulate dynamic changes in the content. $original_html = str_replace( 'dynamic-id-', "dynamic-id-{$num_calls}-", $original_html ); - AMP_Response_Headers::$headers_sent = array(); + AMP_HTTP::$headers_sent = array(); AMP_Validation_Manager::$validation_results = array(); AMP_Theme_Support::prepare_response( $original_html, $args ); @@ -1707,7 +1426,7 @@ private function get_original_html() { */ private function get_server_timing_header_count() { return count( array_filter( - AMP_Response_Headers::$headers_sent, + AMP_HTTP::$headers_sent, function( $header ) { return 'Server-Timing' === $header['name']; } diff --git a/tests/test-tag-and-attribute-sanitizer.php b/tests/test-tag-and-attribute-sanitizer.php index 3eb250a3063..bcb5ed8b2ee 100644 --- a/tests/test-tag-and-attribute-sanitizer.php +++ b/tests/test-tag-and-attribute-sanitizer.php @@ -214,6 +214,76 @@ public function get_body_data() { array( 'amp-playbuzz' ), ), + // AMP-NEXT-PAGE > [separator]. + 'reference-point-amp-next-page-separator' => array( + '

Keep reading

', + null, + array( 'amp-next-page' ), + ), + + // amp-next-page extension .json configuration. + 'reference-point-amp-next-page-json-config' => array( + '', + null, + array( 'amp-next-page' ), + ), + + 'reference-point-amp-carousel-lightbox-exclude' => array( + '', + null, + array( 'amp-carousel', 'amp-lightbox-gallery' ), + ), + + 'reference-point-lightbox-thumbnail-id' => array( + '', + null, + array(), + ), + + 'reference-points-amp-live-list' => array( + '
', + null, + array( 'amp-live-list' ), + ), + + 'reference-points-amp-story' => array( + str_replace( + array( "\n", "\t" ), + '', + ' + + + + + + + +

Hello, amp-story!

+
+
+ + + + + + +

The End

+
+
+ +
+ ' + ), + null, + array( 'amp-story' ), + ), + + 'reference-points-bad' => array( + '
BAD REFERENCE POINTS
', + '
BAD REFERENCE POINTS
', + array(), + ), + 'amp-position-observer' => array( '', null, // No change. @@ -739,8 +809,47 @@ public function get_body_data() { ), // Adapted from . - 'amp_selector_and_carousel_with_boolean_attributes' => array( - '
  • None of the Above
', + 'reference-points-amp_selector_and_carousel_with_boolean_attributes' => array( + str_replace( + array( "\n", "\t" ), + '', + ' +
+ +
    +
  • + +
  • +
  • + +
  • +
  • None of the Above
  • +
+
+ + + + + + + + + + + + + +
+ + + + + + + + + ' + ), null, // No change. array( 'amp-selector', 'amp-form', 'amp-carousel' ), ), @@ -844,7 +953,7 @@ public function get_body_data() { ), 'amp-pan-zoom' => array( - ' ... ', + ' ... ', null, array( 'amp-pan-zoom' ), ), @@ -862,9 +971,9 @@ public function get_body_data() { ), 'amp-lightbox' => array( - '', + '', null, - array( 'amp-lightbox' ), + array( 'amp-lightbox', 'amp-bind' ), ), 'amp-fom-messages' => array( @@ -872,6 +981,18 @@ public function get_body_data() { null, array( 'amp-form', 'amp-mustache' ), ), + + 'amp-viqeo-player' => array( + '', + null, + array( 'amp-viqeo-player' ), + ), + + 'amp-image-slider' => array( + '
This apple is green
This apple is red
', + null, + array( 'amp-image-slider' ), + ), ); } @@ -917,6 +1038,12 @@ public function test_body_sanitizer( $source, $expected = null, $scripts = array $sanitizer->sanitize(); $content = AMP_DOM_Utils::get_content_from_dom( $dom ); $content = preg_replace( '/(?<=>)\s+(?=<)/', '', $content ); + preg_match_all( '#<.+?>#', $expected, $expected_matches ); + preg_match_all( '#<.+?>#', $content, $content_matches ); + $this->assertEquals( + $expected_matches, + $content_matches + ); $this->assertEquals( $expected, $content ); $this->assertEqualSets( $scripts, array_keys( $sanitizer->get_scripts() ) ); } @@ -1017,6 +1144,12 @@ public function test_html_sanitizer( $source, $expected = null, $scripts = array $sanitizer->sanitize(); $content = AMP_DOM_Utils::get_content_from_dom_node( $dom, $dom->documentElement ); $content = preg_replace( '/(?<=>)\s+(?=<)/', '', $content ); + preg_match_all( '#<.+?>#', $expected, $expected_matches ); + preg_match_all( '#<.+?>#', $content, $content_matches ); + $this->assertEquals( + $expected_matches, + $content_matches + ); $this->assertEquals( $expected, $content ); $this->assertEqualSets( $scripts, array_keys( $sanitizer->get_scripts() ) ); } diff --git a/tests/validation/test-class-amp-invalid-url-post-type.php b/tests/validation/test-class-amp-invalid-url-post-type.php index 9d53e0d908a..ea2196c2b4a 100644 --- a/tests/validation/test-class-amp-invalid-url-post-type.php +++ b/tests/validation/test-class-amp-invalid-url-post-type.php @@ -399,6 +399,40 @@ function( $stored_error ) { } } + /** + * Test get_post_staleness method. + * + * @covers AMP_Invalid_URL_Post_Type::get_post_staleness() + */ + public function test_get_post_staleness() { + $error = array( 'code' => 'foo' ); + switch_theme( 'twentysixteen' ); + update_option( 'active_plugins', array( 'foo/foo.php', 'bar.php' ) ); + + $invalid_url_post_id = AMP_Invalid_URL_Post_Type::store_validation_errors( array( $error ), home_url( '/' ) ); + $this->assertInternalType( 'int', $invalid_url_post_id ); + $this->assertEmpty( AMP_Invalid_URL_Post_Type::get_post_staleness( $invalid_url_post_id ) ); + + update_option( 'active_plugins', array( 'foo/foo.php', 'baz.php' ) ); + $staleness = AMP_Invalid_URL_Post_Type::get_post_staleness( $invalid_url_post_id ); + $this->assertNotEmpty( $staleness ); + $this->assertArrayHasKey( 'plugins', $staleness ); + $this->assertArrayNotHasKey( 'theme', $staleness ); + + $this->assertEqualSets( array( 'baz.php' ), $staleness['plugins']['new'] ); + $this->assertEqualSets( array( 'bar.php' ), $staleness['plugins']['old'] ); + + switch_theme( 'twentyseventeen' ); + $next_staleness = AMP_Invalid_URL_Post_Type::get_post_staleness( $invalid_url_post_id ); + $this->assertArrayHasKey( 'theme', $next_staleness ); + $this->assertEquals( 'twentysixteen', $next_staleness['theme'] ); + $this->assertSame( $next_staleness['plugins'], $staleness['plugins'] ); + + // Re-storing results updates freshness. + AMP_Invalid_URL_Post_Type::store_validation_errors( array( $error ), home_url( '/' ), $invalid_url_post_id ); + $this->assertEmpty( AMP_Invalid_URL_Post_Type::get_post_staleness( $invalid_url_post_id ) ); + } + /** * Test filter_views_edit. *