Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Validate height, width & layout attributes #3728

Merged
merged 40 commits into from
Dec 30, 2019
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3bf75cc
Validate height, width & layout attributes
pierlon Nov 14, 2019
f5265de
Convert docblocks to multi-line comments
pierlon Nov 14, 2019
a585fb1
Use stricter checking for undefined attributes
pierlon Nov 14, 2019
855a3c8
Refactor code for PHP 5.4+ compatibility
pierlon Nov 14, 2019
a5aa7d6
Add tests to cover some edge cases
pierlon Nov 14, 2019
a43f046
Add a reference and link to the JS code that the code was adapted fro…
pierlon Nov 14, 2019
4ef85d0
Assert the element has the `amp_layout` spec before validating the la…
pierlon Nov 14, 2019
cb312b6
Correct description of determining a valid responsive layout
pierlon Nov 14, 2019
298c5cc
Typehint `AMP_CSS_Length` function parameters
pierlon Nov 19, 2019
8af168c
Adapt width and height to conform to the AMP layout
pierlon Nov 19, 2019
fca36cb
Refactor conditions for testing layout validation logic
pierlon Nov 20, 2019
c7c271a
Refactor AMP_CSS_Length
pierlon Nov 20, 2019
eac33b7
Add more tests for `fill` layout
pierlon Nov 20, 2019
bed6d28
Update `amp_is_attribute_empty` doc comment
pierlon Nov 21, 2019
2aa4b00
Set `layout` attribute if `data-amp-layout` attribute exists
pierlon Nov 21, 2019
713fec6
Add `AMP_Layout_Sanitizer` to sanitize elements with `layout` or `dat…
pierlon Nov 21, 2019
9ebe64b
Validate layout before sanitizing attributes
pierlon Nov 22, 2019
462d598
Remove `amp_is_attribute_empty` function
pierlon Nov 22, 2019
4838f18
Query only for elements with a width or height attribute
pierlon Nov 26, 2019
c6dafea
Add tests
pierlon Nov 26, 2019
5fc94fb
Ignore elements with `layout` attribute
pierlon Nov 26, 2019
943e86b
Use valid AMP markup for tests
pierlon Nov 28, 2019
b9b2fa1
Set facebook embed layout via `data-amp-layout` to allow layout sanit…
pierlon Nov 28, 2019
87043f8
Make checking for an empty attribute DRY
pierlon Dec 5, 2019
c2fe40d
Apply `fill` layout when the width & height are set to 100% in style …
pierlon Dec 6, 2019
4f9a6e2
Fix PHPCS issues
pierlon Dec 6, 2019
75e9892
Reapply style attribute if there are any descriptors left
pierlon Dec 6, 2019
c152cf9
Unabbreviate `attr_empty`
pierlon Dec 6, 2019
05fbcbf
Fix grammatical errors in comments
pierlon Dec 6, 2019
c015993
Remove HTML markup in docblocks
pierlon Dec 6, 2019
677d26e
Add test for verifying style attribute being maintained when setting …
pierlon Dec 6, 2019
106d0cf
Accommodate for a 100% width or height being defined as style descrip…
pierlon Dec 6, 2019
2c3bfe5
Use error codes to identify the invalid layout detected
pierlon Dec 13, 2019
379f634
Make video in cover block fill the container
westonruter Dec 28, 2019
0e724d1
Add tests for getting dimensions of local video; clean up uploaded files
westonruter Dec 29, 2019
99c7b4a
Fix handling of elements with width/height set to 100%
westonruter Dec 29, 2019
51a5536
Avoid use of WP_Error for representing validation error
westonruter Dec 29, 2019
706674d
Use constants for error codes
westonruter Dec 29, 2019
b27341e
Add test for invalid heights attribute
westonruter Dec 29, 2019
e42411f
Swap PHP 5.4 for 5.6 to run external-http tests
westonruter Dec 30, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions includes/amp-helper-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,7 @@ function amp_get_content_sanitizers( $post = null ) {
'include_manifest_comment' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'always' : 'when_excessive',
],
'AMP_Meta_Sanitizer' => [],
'AMP_Layout_Sanitizer' => [],
'AMP_Tag_And_Attribute_Sanitizer' => [], // Note: This whitelist sanitizer must come at the end to clean up any remaining issues the other sanitizers didn't catch.
];

Expand Down
2 changes: 2 additions & 0 deletions includes/class-amp-autoloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class AMP_Autoloader {
'AMP_Style_Sanitizer' => 'includes/sanitizers/class-amp-style-sanitizer',
'AMP_Script_Sanitizer' => 'includes/sanitizers/class-amp-script-sanitizer',
'AMP_Embed_Sanitizer' => 'includes/sanitizers/class-amp-embed-sanitizer',
'AMP_Layout_Sanitizer' => 'includes/sanitizers/class-amp-layout-sanitizer',
'AMP_Tag_And_Attribute_Sanitizer' => 'includes/sanitizers/class-amp-tag-and-attribute-sanitizer',
'AMP_Video_Sanitizer' => 'includes/sanitizers/class-amp-video-sanitizer',
'AMP_Core_Theme_Sanitizer' => 'includes/sanitizers/class-amp-core-theme-sanitizer',
Expand All @@ -104,6 +105,7 @@ class AMP_Autoloader {
'AMP_Validation_Callback_Wrapper' => 'includes/validation/class-amp-validation-callback-wrapper',
'AMP_Validated_URL_Post_Type' => 'includes/validation/class-amp-validated-url-post-type',
'AMP_Validation_Error_Taxonomy' => 'includes/validation/class-amp-validation-error-taxonomy',
'AMP_CSS_Length' => 'includes/validation/class-amp-css-length',
'AMP_CLI_Namespace' => 'includes/cli/class-amp-cli-namespace',
'AMP_CLI_Validation_Command' => 'includes/cli/class-amp-cli-validation-command',
'AMP_String_Utils' => 'includes/utils/class-amp-string-utils',
Expand Down
23 changes: 23 additions & 0 deletions includes/embeds/class-amp-core-block-handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class AMP_Core_Block_Handler extends AMP_Base_Embed_Handler {
'core/categories' => 'ampify_categories_block',
'core/archives' => 'ampify_archives_block',
'core/video' => 'ampify_video_block',
'core/cover' => 'ampify_cover_block',
];

/**
Expand Down Expand Up @@ -155,4 +156,26 @@ public function ampify_video_block( $block_content, $block ) {

return $block_content;
}

/**
* Ampify cover block.
*
* This specifically fixes the layout of the block when a background video is assigned.
*
* @see \AMP_Video_Sanitizer::filter_video_dimensions()
*
* @param string $block_content The block content about to be appended.
* @param array $block The full block, including name and attributes.
* @return string Filtered block content.
*/
public function ampify_cover_block( $block_content, $block ) {
if ( isset( $block['attrs']['backgroundType'] ) && 'video' === $block['attrs']['backgroundType'] ) {
$block_content = preg_replace(
'/(?<=<video\s)/',
'layout="fill" object-fit="cover" ',
$block_content
);
}
return $block_content;
}
}
12 changes: 5 additions & 7 deletions includes/embeds/class-amp-facebook-embed.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,12 @@ private function get_embed_type( DOMElement $node ) {
private function create_amp_facebook_and_replace_node( Document $dom, DOMElement $node, $embed_type ) {

$attributes = [
'layout' => 'responsive',
'width' => $node->hasAttribute( 'data-width' ) ? $node->getAttribute( 'data-width' ) : $this->DEFAULT_WIDTH,
'height' => $node->hasAttribute( 'data-height' ) ? $node->getAttribute( 'data-height' ) : $this->DEFAULT_HEIGHT,
// The layout sanitizer will convert this to `layout` when being sanitized.
// The data attribute needs to be used so that the layout sanitizer will process it.
'data-amp-layout' => 'responsive',
'width' => $node->hasAttribute( 'data-width' ) ? $node->getAttribute( 'data-width' ) : $this->DEFAULT_WIDTH,
'height' => $node->hasAttribute( 'data-height' ) ? $node->getAttribute( 'data-height' ) : $this->DEFAULT_HEIGHT,
];
if ( '100%' === $attributes['width'] ) {
$attributes['layout'] = 'fixed-height';
$attributes['width'] = 'auto';
}
pierlon marked this conversation as resolved.
Show resolved Hide resolved

$node->removeAttribute( 'data-width' );
$node->removeAttribute( 'data-height' );
Expand Down
46 changes: 33 additions & 13 deletions includes/sanitizers/class-amp-base-sanitizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,17 +248,31 @@ public function sanitize_dimension( $value, $dimension ) {
return '';
}

/**
* Determine if an attribute value is empty.
*
* @param string|null $value Attribute value.
* @return bool True if empty, false if not.
*/
public function is_empty_attribute_value( $value ) {
return ! isset( $value ) || '' === $value;
}

/**
* Sets the layout, and possibly the 'height' and 'width' attributes.
*
* @param array $attributes {
* Attributes.
*
* @type string $bottom
* @type int|string $height
* @type int|string $width
* @type string $sizes
* @type string $class
* @type string $layout
* @type string $left
* @type string $position
* @type string $right
* @type string $style
* @type string $top
* @type int|string $width
* }
* @return array Attributes.
*/
Expand Down Expand Up @@ -312,21 +326,27 @@ public function set_layout( $attributes ) {
&& '100%' === $attributes['width']
&& '100%' === $attributes['height']
) {
unset( $attributes['style'], $attributes['width'], $attributes['height'] );
unset( $attributes['style'], $styles['position'], $attributes['width'], $attributes['height'] );
if ( ! empty( $styles ) ) {
$attributes['style'] = $this->reassemble_style_string( $styles );
}
pierlon marked this conversation as resolved.
Show resolved Hide resolved
$attributes['layout'] = 'fill';
unset( $attributes['height'], $attributes['width'] );
return $attributes;
}
}

if ( empty( $attributes['height'] ) ) {
unset( $attributes['width'] );
$attributes['height'] = self::FALLBACK_HEIGHT;
}

if ( empty( $attributes['width'] ) || '100%' === $attributes['width'] ) {
$attributes['layout'] = 'fixed-height';
$attributes['width'] = 'auto';
if ( isset( $attributes['width'], $attributes['height'] ) && '100%' === $attributes['width'] && '100%' === $attributes['height'] ) {
unset( $attributes['width'], $attributes['height'] );
$attributes['layout'] = 'fill';
} else {
if ( empty( $attributes['height'] ) ) {
unset( $attributes['width'] );
$attributes['height'] = self::FALLBACK_HEIGHT;
}
if ( empty( $attributes['width'] ) || '100%' === $attributes['width'] ) {
$attributes['layout'] = 'fixed-height';
$attributes['width'] = 'auto';
}
}

return $attributes;
Expand Down
118 changes: 118 additions & 0 deletions includes/sanitizers/class-amp-layout-sanitizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php
/**
* Class AMP_Layout_Sanitizer
*
* @since 1.5.0
* @package AMP
*/

/**
* Class AMP_Layout_Sanitizer
*
* @since 1.5.0
*/
class AMP_Layout_Sanitizer extends AMP_Base_Sanitizer {
use AMP_Noscript_Fallback;

/**
* Sanitize any element that has the `layout` or `data-amp-layout` attribute.
*/
public function sanitize() {
$xpath = new DOMXPath( $this->dom );

// Elements with the `layout` attribute will be validated by `AMP_Tag_And_Attribute_Sanitizer`.
$nodes = $xpath->query( '//*[ not( @layout ) and ( @data-amp-layout or @width or @height or @style ) ]' );

foreach ( $nodes as $node ) {
pierlon marked this conversation as resolved.
Show resolved Hide resolved
/**
* Element.
*
* @var DOMElement $node
*/

// Layout does not apply inside of noscript.
if ( $this->is_inside_amp_noscript( $node ) ) {
continue;
}

$width = $node->getAttribute( 'width' );
$height = $node->getAttribute( 'height' );
$style = $node->getAttribute( 'style' );

// The `layout` attribute can also be defined through the `data-amp-layout` attribute.
if ( $node->hasAttribute( 'data-amp-layout' ) ) {
pierlon marked this conversation as resolved.
Show resolved Hide resolved
$layout = $node->getAttribute( 'data-amp-layout' );
$node->setAttribute( 'layout', $layout );
$node->removeAttribute( 'data-amp-layout' );
}

if ( ! $this->is_empty_attribute_value( $style ) ) {
$styles = $this->parse_style_string( $style );

/*
* If both height & width descriptors are 100%, or
* width attribute is 100% and height style descriptor is 100%, or
* height attribute is 100% and width style descriptor is 100%
* then apply fill layout.
*/
if (
(
isset( $styles['width'], $styles['height'] ) &&
( '100%' === $styles['width'] && '100%' === $styles['height'] )
) ||
(
( ! $this->is_empty_attribute_value( $width ) && '100%' === $width ) &&
( isset( $styles['height'] ) && '100%' === $styles['height'] )
) ||
(
( ! $this->is_empty_attribute_value( $height ) && '100%' === $height ) &&
( isset( $styles['width'] ) && '100%' === $styles['width'] )
)
) {
unset( $styles['width'], $styles['height'] );
$node->removeAttribute( 'width' );
$node->removeAttribute( 'height' );

if ( empty( $styles ) ) {
$node->removeAttribute( 'style' );
} else {
$node->setAttribute( 'style', $this->reassemble_style_string( $styles ) );
}

$this->set_layout_fill( $node );
continue;
}
}

// If the width & height are `100%` then apply fill layout.
if ( '100%' === $width && '100%' === $height ) {
$this->set_layout_fill( $node );
continue;
}

pierlon marked this conversation as resolved.
Show resolved Hide resolved
// If the width is `100%`, convert the layout to `fixed-height` and width to `auto`.
if ( '100%' === $width ) {
$node->setAttribute( 'width', 'auto' );
$node->setAttribute( 'layout', AMP_Rule_Spec::LAYOUT_FIXED_HEIGHT );
}
}

$this->did_convert_elements = true;
}

/**
* Apply the `fill` layout.
*
* @param DOMElement $node Node.
*/
private function set_layout_fill( DOMElement $node ) {
if ( $node->hasAttribute( 'width' ) && $node->hasAttribute( 'height' ) ) {
$node->removeAttribute( 'width' );
$node->removeAttribute( 'height' );
}

if ( AMP_Rule_Spec::LAYOUT_FILL !== $node->getAttribute( 'layout' ) ) {
$node->setAttribute( 'layout', AMP_Rule_Spec::LAYOUT_FILL );
}
}
}
22 changes: 17 additions & 5 deletions includes/sanitizers/class-amp-rule-spec.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@
* Set of constants used throughout the sanitizer.
*/
abstract class AMP_Rule_Spec {

/**
/*
* AMP rule_spec types
*/
const ATTR_SPEC_LIST = 'attr_spec_list';
const TAG_SPEC = 'tag_spec';
const CDATA = 'cdata';

/**
/*
* AMP attr_spec value check results.
*
* In 0.7 these changed from strings to integers to speed up comparisons.
Expand All @@ -28,7 +27,7 @@ abstract class AMP_Rule_Spec {
const FAIL = 0;
const NOT_APPLICABLE = -1;

/**
/*
* HTML Element Tag rule names
*/
const DISALLOWED_ANCESTOR = 'disallowed_ancestor';
Expand All @@ -37,7 +36,7 @@ abstract class AMP_Rule_Spec {
const DESCENDANT_TAG_LIST = 'descendant_tag_list';
const CHILD_TAGS = 'child_tags';

/**
/*
* HTML Element Attribute rule names
*/
const ALLOW_EMPTY = 'allow_empty';
Expand All @@ -53,6 +52,19 @@ abstract class AMP_Rule_Spec {
const VALUE_PROPERTIES = 'value_properties';
const VALUE_URL = 'value_url';

/*
* AMP layout types
pierlon marked this conversation as resolved.
Show resolved Hide resolved
*/
const LAYOUT_NODISPLAY = 'nodisplay';
const LAYOUT_FIXED = 'fixed';
const LAYOUT_FIXED_HEIGHT = 'fixed-height';
const LAYOUT_RESPONSIVE = 'responsive';
const LAYOUT_CONTAINER = 'container';
const LAYOUT_FILL = 'fill';
const LAYOUT_FLEX_ITEM = 'flex-item';
const LAYOUT_FLUID = 'fluid';
const LAYOUT_INTRINSIC = 'intrinsic';

/**
* Attribute name for AMP dev mode.
*
Expand Down
Loading