diff --git a/.travis.yml b/.travis.yml
index eb94773397a..9675ab1ab36 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -39,6 +39,7 @@ matrix:
env: WP_VERSION=trunk
install:
+ - if [[ $DEV_LIB_SKIP =~ composer ]]; then composer install --no-dev; fi
- nvm install 6 && nvm use 6
- export DEV_LIB_PATH=dev-lib
- source $DEV_LIB_PATH/travis.install.sh
diff --git a/Gruntfile.js b/Gruntfile.js
index 24f1475ee3b..09b3eec6eb9 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -84,15 +84,21 @@ module.exports = function( grunt ) {
args: [ 'ls-files' ]
},
function( err, res ) {
+ var paths;
if ( err ) {
throw new Error( err.message );
}
+ paths = res.stdout.trim().split( /\n/ ).filter( function( file ) {
+ return ! /^(\.|bin|([^/]+)+\.(md|json|xml)|Gruntfile\.js|tests|wp-assets|dev-lib|readme\.md|composer\..*)/.test( file );
+ } );
+ paths.push( 'vendor/autoload.php' );
+ paths.push( 'vendor/composer/**' );
+ paths.push( 'vendor/sabberworm/php-css-parser/lib/**' );
+
grunt.config.set( 'copy', {
build: {
- src: res.stdout.trim().split( /\n/ ).filter( function( file ) {
- return ! /^(\.|bin|([^/]+)+\.(md|json|xml)|Gruntfile\.js|tests|wp-assets|dev-lib|readme\.md|composer\..*)/.test( file );
- } ),
+ src: paths,
dest: 'build',
expand: true
}
diff --git a/amp.php b/amp.php
index 6481ebcb119..860e4371cd9 100644
--- a/amp.php
+++ b/amp.php
@@ -21,15 +21,32 @@
function _amp_print_php_version_admin_notice() {
?>
+
+
+
+ endpoints as $index => $endpoint ) {
if ( amp_get_slug() === $endpoint[1] ) {
@@ -130,8 +159,16 @@ function amp_init() {
add_action( 'wp', 'amp_maybe_add_actions' );
}
-// Make sure the `amp` query var has an explicit value.
-// Avoids issues when filtering the deprecated `query_string` hook.
+/**
+ * Make sure the `amp` query var has an explicit value.
+ *
+ * This avoids issues when filtering the deprecated `query_string` hook.
+ *
+ * @since 0.3.3
+ *
+ * @param array $query_vars Query vars.
+ * @return array Query vars.
+ */
function amp_force_query_var_value( $query_vars ) {
if ( isset( $query_vars[ amp_get_slug() ] ) && '' === $query_vars[ amp_get_slug() ] ) {
$query_vars[ amp_get_slug() ] = 1;
@@ -250,20 +287,41 @@ function amp_is_canonical() {
return false;
}
+/**
+ * Load classes.
+ *
+ * @since 0.2
+ * @deprecated As of 0.6 since autoloading is now employed.
+ */
function amp_load_classes() {
_deprecated_function( __FUNCTION__, '0.6' );
}
+/**
+ * Add frontend actions.
+ *
+ * @since 0.2
+ */
function amp_add_frontend_actions() {
require_once AMP__DIR__ . '/includes/amp-frontend-actions.php';
}
+/**
+ * Add post template actions.
+ *
+ * @since 0.2
+ */
function amp_add_post_template_actions() {
require_once AMP__DIR__ . '/includes/amp-post-template-actions.php';
require_once AMP__DIR__ . '/includes/amp-post-template-functions.php';
amp_post_template_init_hooks();
}
+/**
+ * Add action to do post template rendering at template_redirect action.
+ *
+ * @since 0.2
+ */
function amp_prepare_render() {
add_action( 'template_redirect', 'amp_render' );
}
diff --git a/bin/amphtml-update.py b/bin/amphtml-update.py
index 399d2e07377..15d609a6454 100644
--- a/bin/amphtml-update.py
+++ b/bin/amphtml-update.py
@@ -356,12 +356,37 @@ def GetTagSpec(tag_spec, attr_lists):
for (field_descriptor, field_value) in tag_spec.cdata.ListFields():
if isinstance(field_value, (unicode, str, bool, int)):
cdata_dict[ field_descriptor.name ] = field_value
- else:
- if hasattr( field_value, '_values' ):
- cdata_dict[ field_descriptor.name ] = {}
- for _value in field_value._values:
- for (key,val) in _value.ListFields():
- cdata_dict[ field_descriptor.name ][ key.name ] = val
+ elif hasattr( field_value, '_values' ):
+ cdata_dict[ field_descriptor.name ] = {}
+ for _value in field_value._values:
+ for (key,val) in _value.ListFields():
+ cdata_dict[ field_descriptor.name ][ key.name ] = val
+ elif 'css_spec' == field_descriptor.name:
+ css_spec = {}
+
+ css_spec['allowed_at_rules'] = []
+ for at_rule_spec in field_value.at_rule_spec:
+ if '$DEFAULT' == at_rule_spec.name:
+ continue
+ css_spec['allowed_at_rules'].append( at_rule_spec.name )
+
+ for css_spec_field_name in ( 'allowed_declarations', 'font_url_spec', 'image_url_spec', 'validate_keyframes' ):
+ if not hasattr( field_value, css_spec_field_name ):
+ continue
+ css_spec_field_value = getattr( field_value, css_spec_field_name )
+ if isinstance(css_spec_field_value, (list, collections.Sequence, google.protobuf.internal.containers.RepeatedScalarFieldContainer)):
+ css_spec[ css_spec_field_name ] = [ val for val in css_spec_field_value ]
+ elif hasattr( css_spec_field_value, 'ListFields' ):
+ css_spec[ css_spec_field_name ] = {}
+ for (css_spec_field_item_descriptor, css_spec_field_item_value) in getattr( field_value, css_spec_field_name ).ListFields():
+ if isinstance(css_spec_field_item_value, (list, collections.Sequence, google.protobuf.internal.containers.RepeatedScalarFieldContainer)):
+ css_spec[ css_spec_field_name ][ css_spec_field_item_descriptor.name ] = [ val for val in css_spec_field_item_value ]
+ else:
+ css_spec[ css_spec_field_name ][ css_spec_field_item_descriptor.name ] = css_spec_field_item_value
+ else:
+ css_spec[ css_spec_field_name ] = css_spec_field_value
+
+ cdata_dict['css_spec'] = css_spec
if len( cdata_dict ) > 0:
tag_spec_dict['cdata'] = cdata_dict
diff --git a/composer.json b/composer.json
index 33409a6e7de..bf538e3783b 100644
--- a/composer.json
+++ b/composer.json
@@ -5,6 +5,15 @@
"type": "wordpress-plugin",
"license": "GPL-2.0",
"version": "1.0.0",
+ "repositories": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/xwp/PHP-CSS-Parser"
+ }
+ ],
+ "require": {
+ "sabberworm/php-css-parser": "dev-master"
+ },
"require-dev": {
"wp-coding-standards/wpcs": "^0.14.0",
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.4",
diff --git a/composer.lock b/composer.lock
index de486f97bc5..4579f0f7cd1 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,55 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "content-hash": "984ff0ea17c1c8dff71e6e2e2e9dc8a0",
- "packages": [],
+ "content-hash": "3e2682fa9441981321b1f903b75abb25",
+ "packages": [
+ {
+ "name": "sabberworm/php-css-parser",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/xwp/PHP-CSS-Parser.git",
+ "reference": "e3204589287c28396b3db16b92ec30dab19ac2e9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/xwp/PHP-CSS-Parser/zipball/e3204589287c28396b3db16b92ec30dab19ac2e9",
+ "reference": "e3204589287c28396b3db16b92ec30dab19ac2e9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.8"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Sabberworm\\CSS": "lib/"
+ }
+ },
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Raphael Schweikert"
+ }
+ ],
+ "description": "Parser for CSS Files written in PHP",
+ "homepage": "http://www.sabberworm.com/blog/2010/6/10/php-css-parser",
+ "keywords": [
+ "css",
+ "parser",
+ "stylesheet"
+ ],
+ "support": {
+ "source": "https://github.com/xwp/PHP-CSS-Parser/tree/master"
+ },
+ "time": "2018-04-01 07:35:36"
+ }
+ ],
"packages-dev": [
{
"name": "dealerdirect/phpcodesniffer-composer-installer",
@@ -77,16 +124,16 @@
},
{
"name": "squizlabs/php_codesniffer",
- "version": "3.2.2",
+ "version": "3.2.3",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
- "reference": "d7c00c3000ac0ce79c96fcbfef86b49a71158cd1"
+ "reference": "4842476c434e375f9d3182ff7b89059583aa8b27"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d7c00c3000ac0ce79c96fcbfef86b49a71158cd1",
- "reference": "d7c00c3000ac0ce79c96fcbfef86b49a71158cd1",
+ "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/4842476c434e375f9d3182ff7b89059583aa8b27",
+ "reference": "4842476c434e375f9d3182ff7b89059583aa8b27",
"shasum": ""
},
"require": {
@@ -96,7 +143,7 @@
"php": ">=5.4.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0"
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"bin": [
"bin/phpcs",
@@ -124,7 +171,7 @@
"phpcs",
"standards"
],
- "time": "2017-12-19T21:44:46+00:00"
+ "time": "2018-02-20T21:35:23+00:00"
},
{
"name": "wimg/php-compatibility",
@@ -180,16 +227,16 @@
},
{
"name": "wp-coding-standards/wpcs",
- "version": "0.14.0",
+ "version": "0.14.1",
"source": {
"type": "git",
"url": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards.git",
- "reference": "8cadf48fa1c70b2381988e0a79e029e011a8f41c"
+ "reference": "cf6b310caad735816caef7573295f8a534374706"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/WordPress-Coding-Standards/WordPress-Coding-Standards/zipball/8cadf48fa1c70b2381988e0a79e029e011a8f41c",
- "reference": "8cadf48fa1c70b2381988e0a79e029e011a8f41c",
+ "url": "https://api.github.com/repos/WordPress-Coding-Standards/WordPress-Coding-Standards/zipball/cf6b310caad735816caef7573295f8a534374706",
+ "reference": "cf6b310caad735816caef7573295f8a534374706",
"shasum": ""
},
"require": {
@@ -216,12 +263,14 @@
"standards",
"wordpress"
],
- "time": "2017-11-01T15:10:46+00:00"
+ "time": "2018-02-16T01:57:48+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": [],
+ "stability-flags": {
+ "sabberworm/php-css-parser": 20
+ },
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
diff --git a/contributing.md b/contributing.md
index ba5f791c223..6dfae576415 100644
--- a/contributing.md
+++ b/contributing.md
@@ -2,11 +2,22 @@
Thanks for taking the time to contribute!
-To clone this repository
-``` bash
-$ git clone --recursive git@github.com:Automattic/amp-wp.git
+To start, clone this repository into your WordPress install being used for development:
+
+```bash
+cd wp-content/plugins && git clone --recursive git@github.com:Automattic/amp-wp.git amp
```
+If you happened to have cloned without `--recursive` previously, please do `git submodule update --init` to ensure the [dev-lib](https://github.com/xwp/wp-dev-lib/) submodule is available for development.
+
+Lastly, to get the plugin running in your WordPress install, run `composer install` and then activate the plugin via the WordPress dashboard or `wp plugin activate amp`.
+
+To install the `pre-commit` hook, do `bash dev-lib/install-pre-commit-hook.sh`.
+
+Note that pull requests will be checked against [WordPress-Coding-Standards](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards) with PHPCS, and for JavaScript linting is done with ESLint and (for now) JSCS and JSHint.
+
+To run the Grunt commands, please first `npm install -g grunt-cli` and then `npm install`.
+
## Updating Allowed Tags And Attributes
The file `class-amp-allowed-tags-generated.php` has the AMP specification's allowed tags and attributes. It's used in sanitization.
@@ -72,6 +83,7 @@ When you push a commit to your PR, Travis CI will run the PHPUnit tests and snif
Contributors who want to make a new release, follow these steps:
+0. Do `grunt build` and install the `amp.zip` onto a normal WordPress install running a stable release build; do smoke test to ensure it works.
1. Bump plugin versions in `package.json` (×1), `package-lock.json` (×1, just do `npm install` first), `composer.json` (×1), and in `amp.php` (×2: the metadata block in the header and also the `AMP__VERSION` constant).
2. Add changelog entry to readme.
3. Merge release branch into `master`.
diff --git a/includes/amp-post-template-actions.php b/includes/amp-post-template-actions.php
index b956faaf6c6..88d717c2c6e 100644
--- a/includes/amp-post-template-actions.php
+++ b/includes/amp-post-template-actions.php
@@ -107,6 +107,12 @@ function amp_post_template_add_schemaorg_metadata() {
* @param AMP_Post_Template $amp_template Template.
*/
function amp_post_template_add_styles( $amp_template ) {
+ $stylesheets = $amp_template->get( 'post_amp_stylesheets' );
+ if ( ! empty( $stylesheets ) ) {
+ echo '/* Inline stylesheets */' . PHP_EOL; // WPCS: XSS OK.
+ echo implode( '', $stylesheets ); // WPCS: XSS OK.
+ }
+
$styles = $amp_template->get( 'post_amp_styles' );
if ( ! empty( $styles ) ) {
echo '/* Inline styles */' . PHP_EOL; // WPCS: XSS OK.
diff --git a/includes/class-amp-autoloader.php b/includes/class-amp-autoloader.php
index b98afe871be..c86307f0a77 100644
--- a/includes/class-amp-autoloader.php
+++ b/includes/class-amp-autoloader.php
@@ -30,6 +30,7 @@ class AMP_Autoloader {
*/
private static $_classmap = array(
'AMP_Theme_Support' => 'includes/class-amp-theme-support',
+ 'AMP_Response_Headers' => 'includes/class-amp-response-headers',
'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',
@@ -130,6 +131,10 @@ protected static function autoload( $class_name ) {
* Called at the end of this file; calling a second time has no effect.
*/
public static function register() {
+ if ( file_exists( AMP__DIR__ . '/vendor/autoload.php' ) ) {
+ require_once AMP__DIR__ . '/vendor/autoload.php';
+ }
+
if ( ! self::$is_registered ) {
spl_autoload_register( array( __CLASS__, 'autoload' ) );
self::$is_registered = true;
diff --git a/includes/class-amp-response-headers.php b/includes/class-amp-response-headers.php
new file mode 100644
index 00000000000..d8a5e62d635
--- /dev/null
+++ b/includes/class-amp-response-headers.php
@@ -0,0 +1,88 @@
+ 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.
+ *
+ * @since 1.0
+ * @todo What is the ordering in Chrome dev tools? What are the colors about?
+ * @todo Is there a better name standardization?
+ * @todo Is there a way to indicate nested server timings, so an outer method's own time can be seen separately from the inner method's time?
+ *
+ * @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.
+ */
+ public static function send_server_timing( $name, $duration = null, $description = null ) {
+ $value = $name;
+ if ( isset( $description ) ) {
+ $value .= sprintf( ';desc=%s', wp_json_encode( $description ) );
+ }
+ 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-theme-support.php b/includes/class-amp-theme-support.php
index 49479a0b401..01207b645ca 100644
--- a/includes/class-amp-theme-support.php
+++ b/includes/class-amp-theme-support.php
@@ -68,22 +68,25 @@ class AMP_Theme_Support {
public static $purged_amp_query_vars = array();
/**
- * Headers sent (or attempted to be sent).
+ * Start time when init was called.
*
- * @since 0.7
- * @see AMP_Theme_Support::send_header()
- * @var array[]
+ * @since 1.0
+ * @var float
*/
- public static $headers_sent = array();
+ public static $init_start_time;
/**
* Initialize.
+ *
+ * @since 0.7
*/
public static function init() {
if ( ! current_theme_supports( 'amp' ) ) {
return;
}
+ self::$init_start_time = microtime( true );
+
self::purge_amp_query_vars();
self::handle_xhr_request();
@@ -322,45 +325,6 @@ public static function purge_amp_query_vars() {
}
}
- /**
- * Send an HTTP response header.
- *
- * This largely exists to facilitate unit testing but it also provides a better interface for sending headers.
- *
- * @since 0.7.0
- *
- * @param string $name Header name.
- * @param string $value Header value.
- * @param array $args {
- * Args to header().
- *
- * @type bool $replace Whether to replace a header previously sent. Default true.
- * @type int $status_code Status code to send with the sent header.
- * }
- * @return bool Whether the header was sent.
- */
- public static function send_header( $name, $value, $args = array() ) {
- $args = array_merge(
- array(
- 'replace' => 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;
- }
-
/**
* Hook into a POST form submissions, such as the comment form or some other form submission.
*
@@ -381,7 +345,7 @@ public static function handle_xhr_request() {
// Send AMP response header.
$origin = wp_validate_redirect( wp_sanitize_redirect( esc_url_raw( self::$purged_amp_query_vars['__amp_source_origin'] ) ) );
if ( $origin ) {
- self::send_header( 'AMP-Access-Control-Allow-Source-Origin', $origin, array( 'replace' => true ) );
+ AMP_Response_Headers::send_header( 'AMP-Access-Control-Allow-Source-Origin', $origin, array( 'replace' => true ) );
}
// Intercept POST requests which redirect.
@@ -530,8 +494,8 @@ public static function intercept_post_request_redirect( $location ) {
$absolute_location .= '#' . $parsed_location['fragment'];
}
- self::send_header( 'AMP-Redirect-To', $absolute_location );
- self::send_header( 'Access-Control-Expose-Headers', 'AMP-Redirect-To' );
+ 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();
}
@@ -972,6 +936,7 @@ public static function start_output_buffering() {
* @see AMP_Theme_Support::start_output_buffering()
*/
public static function finish_output_buffering() {
+ AMP_Response_Headers::send_server_timing( 'amp_output_buffer', -self::$init_start_time, 'AMP Output Buffer' );
echo self::prepare_response( ob_get_clean() ); // WPCS: xss ok.
}
@@ -1039,6 +1004,8 @@ public static function prepare_response( $response, $args = array() ) {
$args
);
+ $dom_parse_start = microtime( true );
+
/*
* Make sure that is present in output prior to parsing.
* Note that the meta charset is supposed to appear within the first 1024 bytes.
@@ -1070,8 +1037,11 @@ 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' );
+
$assets = AMP_Content_Sanitizer::sanitize_document( $dom, self::$sanitizer_classes, $args );
+ $dom_serialize_start = microtime( true );
self::ensure_required_markup( $dom );
// @todo If 'utf-8' is not the blog charset, then we'll need to do some character encoding conversation or "entityification".
@@ -1117,6 +1087,8 @@ public static function prepare_response( $response, $args = array() ) {
);
}
+ AMP_Response_Headers::send_server_timing( 'amp_dom_serialize', -$dom_serialize_start, 'AMP DOM Serialize' );
+
return $response;
}
diff --git a/includes/sanitizers/class-amp-allowed-tags-generated.php b/includes/sanitizers/class-amp-allowed-tags-generated.php
index d7e28aecf92..f666c891ae0 100644
--- a/includes/sanitizers/class-amp-allowed-tags-generated.php
+++ b/includes/sanitizers/class-amp-allowed-tags-generated.php
@@ -9180,6 +9180,35 @@ class AMP_Allowed_Tags_Generated {
'error_message' => 'CSS !important',
'regex' => '!important',
),
+ 'css_spec' => array(
+ 'allowed_at_rules' => array(
+ 'font-face',
+ 'keyframes',
+ 'media',
+ 'supports',
+ ),
+ 'allowed_declarations' => array(),
+ 'font_url_spec' => array(
+ 'allow_empty' => true,
+ 'allow_relative' => true,
+ 'allowed_protocol' => array(
+ 'https',
+ 'http',
+ 'data',
+ ),
+ ),
+ 'image_url_spec' => array(
+ 'allow_empty' => true,
+ 'allow_relative' => true,
+ 'allowed_protocol' => array(
+ 'https',
+ 'http',
+ 'data',
+ 'absolute',
+ ),
+ ),
+ 'validate_keyframes' => false,
+ ),
'max_bytes' => 50000,
'max_bytes_spec_url' => 'https://www.ampproject.org/docs/reference/spec#maximum-size',
),
@@ -9240,6 +9269,23 @@ class AMP_Allowed_Tags_Generated {
),
),
'cdata' => array(
+ 'css_spec' => array(
+ 'allowed_at_rules' => array(
+ 'keyframes',
+ 'media',
+ 'supports',
+ ),
+ 'allowed_declarations' => array(
+ 'animation-timing-function',
+ 'offset-distance',
+ 'opacity',
+ 'transform',
+ 'visibility',
+ ),
+ 'font_url_spec' => array(),
+ 'image_url_spec' => array(),
+ 'validate_keyframes' => true,
+ ),
'max_bytes' => 500000,
'max_bytes_spec_url' => 'https://www.ampproject.org/docs/reference/spec#keyframes-stylesheet',
),
diff --git a/includes/sanitizers/class-amp-base-sanitizer.php b/includes/sanitizers/class-amp-base-sanitizer.php
index 0fc9a697085..1c2cb183be6 100644
--- a/includes/sanitizers/class-amp-base-sanitizer.php
+++ b/includes/sanitizers/class-amp-base-sanitizer.php
@@ -55,7 +55,7 @@ abstract class AMP_Base_Sanitizer {
* @type bool $allow_dirty_styles
* @type bool $allow_dirty_scripts
* @type bool $disable_invalid_removal
- * @type callable $remove_invalid_callback
+ * @type callable $validation_error_callback
* }
*/
protected $args;
@@ -130,6 +130,7 @@ public function get_scripts() {
* Return array of values that would be valid as an HTML `style` attribute.
*
* @since 0.4
+ * @deprecated As of 1.0, use get_stylesheets().
*
* @return array[][] Mapping of CSS selectors to arrays of properties.
*/
diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php
index fe07b47e46c..8440c097369 100644
--- a/includes/sanitizers/class-amp-style-sanitizer.php
+++ b/includes/sanitizers/class-amp-style-sanitizer.php
@@ -5,6 +5,19 @@
* @package AMP
*/
+use \Sabberworm\CSS\RuleSet\DeclarationBlock;
+use \Sabberworm\CSS\CSSList\CSSList;
+use \Sabberworm\CSS\Property\Selector;
+use \Sabberworm\CSS\RuleSet\RuleSet;
+use \Sabberworm\CSS\Rule\Rule;
+use \Sabberworm\CSS\Property\AtRule;
+use \Sabberworm\CSS\CSSList\KeyFrame;
+use \Sabberworm\CSS\RuleSet\AtRuleSet;
+use \Sabberworm\CSS\Property\Import;
+use \Sabberworm\CSS\CSSList\AtRuleBlockList;
+use \Sabberworm\CSS\Value\RuleValueList;
+use \Sabberworm\CSS\Value\URL;
+
/**
* Class AMP_Style_Sanitizer
*
@@ -13,19 +26,38 @@
class AMP_Style_Sanitizer extends AMP_Base_Sanitizer {
/**
- * Styles.
+ * Array of flags used to control sanitization.
*
- * List of CSS styles in HTML content of DOMDocument ($this->dom).
+ * @var array {
+ * @type string $remove_unused_rules Enum 'never', 'sometimes' (default), 'always'. If total CSS is greater than max_bytes, whether to strip selectors (and then empty rules) when they are not found to be used in doc. A validation error will be emitted when stripping happens since it is not completely safe in the case of dynamic content.
+ * @type string[] $dynamic_element_selectors Selectors for elements (or their ancestors) which contain dynamic content; selectors containing these will not be filtered.
+ * @type bool $use_document_element Whether the root of the document should be used rather than the body.
+ * @type bool $require_https_src Require HTTPS URLs.
+ * @type bool $allow_dirty_styles Allow dirty styles. This short-circuits the sanitize logic; it is used primarily in Customizer preview.
+ * @type callable $validation_error_callback Function to call when a validation error is encountered.
+ * }
+ */
+ protected $args;
+
+ /**
+ * Default args.
*
- * @since 0.4
- * @var array[]
+ * @var array
*/
- private $styles = array();
+ protected $DEFAULT_ARGS = array(
+ 'remove_unused_rules' => 'sometimes',
+ 'dynamic_element_selectors' => array(
+ 'amp-list',
+ 'amp-live-list',
+ '[submit-error]',
+ '[submit-success]',
+ ),
+ );
/**
* Stylesheets.
*
- * Values are the CSS stylesheets. Keys are MD5 hashes of the stylesheets
+ * Values are the CSS stylesheets. Keys are MD5 hashes of the stylesheets,
*
* @since 0.7
* @var string[]
@@ -33,29 +65,27 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer {
private $stylesheets = array();
/**
- * Maximum number of bytes allowed for a keyframes style.
+ * List of stylesheet parts prior to selector/rule removal (tree shaking).
*
- * @since 0.7
- * @var int
- */
- private $keyframes_max_size;
-
- /**
- * Maximum number of bytes allowed for a AMP Custom style.
+ * Keys are MD5 hashes of stylesheets.
*
- * @since 0.7
- * @var int
+ * @since 1.0
+ * @var array[] {
+ * @type array $stylesheet Array of stylesheet chunked, with declaration blocks being represented as arrays.
+ * @type DOMElement|DOMAttr $node Origin for styles.
+ * @type array $sources Sources for the node.
+ * @type bool $keyframes Whether an amp-keyframes.
+ * }
*/
- private $custom_max_size;
+ private $pending_stylesheets = array();
/**
- * Current CSS size.
+ * Spec for style[amp-custom] cdata.
*
- * Sum of CSS located in $styles and $stylesheets.
- *
- * @var int
+ * @since 1.0
+ * @var array
*/
- private $current_custom_size = 0;
+ private $style_custom_cdata_spec;
/**
* The style[amp-custom] element.
@@ -64,6 +94,14 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer {
*/
private $amp_custom_style_element;
+ /**
+ * Spec for style[amp-keyframes] cdata.
+ *
+ * @since 1.0
+ * @var array
+ */
+ private $style_keyframes_cdata_spec;
+
/**
* Regex for allowed font stylesheet URL.
*
@@ -87,6 +125,30 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer {
*/
private $content_url;
+ /**
+ * Class names used in document.
+ *
+ * @since 1.0
+ * @var array
+ */
+ private $used_class_names = array();
+
+ /**
+ * XPath.
+ *
+ * @since 1.0
+ * @var DOMXPath
+ */
+ private $xpath;
+
+ /**
+ * Amount of time that was spent parsing CSS.
+ *
+ * @since 1.0
+ * @var float
+ */
+ private $parse_css_duration = 0.0;
+
/**
* AMP_Base_Sanitizer constructor.
*
@@ -98,19 +160,14 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer {
public function __construct( DOMDocument $dom, array $args = array() ) {
parent::__construct( $dom, $args );
- $spec_name = 'style[amp-keyframes]';
foreach ( AMP_Allowed_Tags_Generated::get_allowed_tag( 'style' ) as $spec_rule ) {
- if ( isset( $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) && $spec_name === $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) {
- $this->keyframes_max_size = $spec_rule[ AMP_Rule_Spec::CDATA ]['max_bytes'];
- break;
+ if ( ! isset( $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) ) {
+ continue;
}
- }
-
- $spec_name = 'style amp-custom';
- foreach ( AMP_Allowed_Tags_Generated::get_allowed_tag( 'style' ) as $spec_rule ) {
- if ( isset( $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) && $spec_name === $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) {
- $this->custom_max_size = $spec_rule[ AMP_Rule_Spec::CDATA ]['max_bytes'];
- break;
+ if ( 'style[amp-keyframes]' === $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) {
+ $this->style_keyframes_cdata_spec = $spec_rule[ AMP_Rule_Spec::CDATA ];
+ } elseif ( 'style amp-custom' === $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) {
+ $this->style_custom_cdata_spec = $spec_rule[ AMP_Rule_Spec::CDATA ];
}
}
@@ -128,20 +185,19 @@ public function __construct( DOMDocument $dom, array $args = array() ) {
}
$this->base_url = $guessurl;
$this->content_url = WP_CONTENT_URL;
+ $this->xpath = new DOMXPath( $dom );
}
/**
* Get list of CSS styles in HTML content of DOMDocument ($this->dom).
*
* @since 0.4
+ * @deprecated As of 1.0, use get_stylesheets().
*
* @return array[] Mapping CSS selectors to array of properties, or mapping of keys starting with 'stylesheet:' with value being the stylesheet.
*/
public function get_styles() {
- if ( ! $this->did_convert_elements ) {
- return array();
- }
- return $this->styles;
+ return array();
}
/**
@@ -151,7 +207,24 @@ public function get_styles() {
* @returns array Values are the CSS stylesheets. Keys are MD5 hashes of the stylesheets.
*/
public function get_stylesheets() {
- return array_merge( $this->stylesheets, parent::get_stylesheets() );
+ return $this->stylesheets;
+ }
+
+ /**
+ * Get list of all the class names used in the document.
+ *
+ * @since 1.0
+ * @return array Used class names.
+ */
+ private function get_used_class_names() {
+ if ( empty( $this->used_class_names ) ) {
+ $classes = ' ';
+ foreach ( $this->xpath->query( '//*/@class' ) as $class_attribute ) {
+ $classes .= ' ' . $class_attribute->nodeValue;
+ }
+ $this->used_class_names = array_unique( array_filter( preg_split( '/\s+/', trim( $classes ) ) ) );
+ }
+ return $this->used_class_names;
}
/**
@@ -167,11 +240,13 @@ public function sanitize() {
return;
}
+ $this->parse_css_duration = 0.0;
+
/*
* Note that xpath is used to query the DOM so that the link and style elements will be
* in document order. DOMNode::compareDocumentPosition() is not yet implemented.
*/
- $xpath = new DOMXPath( $this->dom );
+ $xpath = $this->xpath;
$lower_case = 'translate( %s, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz" )'; // In XPath 2.0 this is lower-case().
$predicates = array(
@@ -204,83 +279,68 @@ public function sanitize() {
foreach ( $elements as $element ) {
$this->collect_inline_styles( $element );
}
- $this->did_convert_elements = true;
- // Now make sure the amp-custom style is in the DOM and populated, if we're working with the document element.
- if ( ! empty( $this->args['use_document_element'] ) ) {
- if ( ! $this->amp_custom_style_element ) {
- $this->amp_custom_style_element = $this->dom->createElement( 'style' );
- $this->amp_custom_style_element->setAttribute( 'amp-custom', '' );
- $head = $this->dom->getElementsByTagName( 'head' )->item( 0 );
- if ( ! $head ) {
- $head = $this->dom->createElement( 'head' );
- $this->dom->documentElement->insertBefore( $head, $this->dom->documentElement->firstChild );
- }
- $head->appendChild( $this->amp_custom_style_element );
- }
+ $this->finalize_styles();
- $css = implode( '', $this->get_stylesheets() );
+ $this->did_convert_elements = true;
- /*
- * Let the style[amp-custom] be populated with the concatenated CSS.
- * !important: Updating the contents of this style element by setting textContent is not
- * reliable across PHP/libxml versions, so this is why the children are removed and the
- * text node is then explicitly added containing the CSS.
- */
- while ( $this->amp_custom_style_element->firstChild ) {
- $this->amp_custom_style_element->removeChild( $this->amp_custom_style_element->firstChild );
- }
- $this->amp_custom_style_element->appendChild( $this->dom->createTextNode( $css ) );
+ if ( $this->parse_css_duration > 0.0 ) {
+ AMP_Response_Headers::send_server_timing( 'amp_parse_css', $this->parse_css_duration, 'AMP Parse CSS' );
}
}
/**
- * Generates an enqueued style's fully-qualified file path.
+ * Generate a URL's fully-qualified file path.
*
* @since 0.7
* @see WP_Styles::_css_href()
*
- * @param string $src The source URL of the enqueued style.
+ * @param string $url The file URL.
+ * @param string[] $allowed_extensions Allowed file extensions.
* @return string|WP_Error Style's absolute validated filesystem path, or WP_Error when error.
*/
- public function get_validated_css_file_path( $src ) {
+ public function get_validated_url_file_path( $url, $allowed_extensions = array() ) {
$needs_base_url = (
- ! is_bool( $src )
+ ! is_bool( $url )
&&
- ! preg_match( '|^(https?:)?//|', $src )
+ ! preg_match( '|^(https?:)?//|', $url )
&&
- ! ( $this->content_url && 0 === strpos( $src, $this->content_url ) )
+ ! ( $this->content_url && 0 === strpos( $url, $this->content_url ) )
);
if ( $needs_base_url ) {
- $src = $this->base_url . $src;
+ $url = $this->base_url . $url;
}
// Strip query and fragment from URL.
- $src = preg_replace( ':[\?#].*$:', '', $src );
-
- if ( ! preg_match( '/\.(css|less|scss|sass)$/i', $src ) ) {
- /* translators: %s is stylesheet URL */
- return new WP_Error( 'amp_css_bad_file_extension', sprintf( __( 'Skipped stylesheet which does not have recognized CSS file extension (%s).', 'amp' ), $src ) );
+ $url = preg_replace( ':[\?#].*$:', '', $url );
+
+ // Validate file extensions.
+ if ( ! empty( $allowed_extensions ) ) {
+ $pattern = sprintf( '/\.(%s)$/i', implode( '|', $allowed_extensions ) );
+ if ( ! preg_match( $pattern, $url ) ) {
+ /* translators: %s is the file URL */
+ return new WP_Error( 'amp_disallowed_file_extension', sprintf( __( 'Skipped file which does not have an allowed file extension (%s).', 'amp' ), $url ) );
+ }
}
$includes_url = includes_url( '/' );
$content_url = content_url( '/' );
$admin_url = get_admin_url( null, '/' );
- $css_path = null;
- if ( 0 === strpos( $src, $content_url ) ) {
- $css_path = WP_CONTENT_DIR . substr( $src, strlen( $content_url ) - 1 );
- } elseif ( 0 === strpos( $src, $includes_url ) ) {
- $css_path = ABSPATH . WPINC . substr( $src, strlen( $includes_url ) - 1 );
- } elseif ( 0 === strpos( $src, $admin_url ) ) {
- $css_path = ABSPATH . 'wp-admin' . substr( $src, strlen( $admin_url ) - 1 );
+ $file_path = null;
+ if ( 0 === strpos( $url, $content_url ) ) {
+ $file_path = WP_CONTENT_DIR . substr( $url, strlen( $content_url ) - 1 );
+ } elseif ( 0 === strpos( $url, $includes_url ) ) {
+ $file_path = ABSPATH . WPINC . substr( $url, strlen( $includes_url ) - 1 );
+ } elseif ( 0 === strpos( $url, $admin_url ) ) {
+ $file_path = ABSPATH . 'wp-admin' . substr( $url, strlen( $admin_url ) - 1 );
}
- if ( ! $css_path || false !== strpos( '../', $css_path ) || 0 !== validate_file( $css_path ) || ! file_exists( $css_path ) ) {
- /* translators: %s is stylesheet URL */
- return new WP_Error( 'amp_css_path_not_found', sprintf( __( 'Unable to locate filesystem path for stylesheet %s.', 'amp' ), $src ) );
+ if ( ! $file_path || false !== strpos( '../', $file_path ) || 0 !== validate_file( $file_path ) || ! file_exists( $file_path ) ) {
+ /* translators: %s is file URL */
+ return new WP_Error( 'amp_file_path_not_found', sprintf( __( 'Unable to locate filesystem path for %s.', 'amp' ), $url ) );
}
- return $css_path;
+ return $file_path;
}
/**
@@ -289,30 +349,29 @@ public function get_validated_css_file_path( $src ) {
* @param DOMElement $element Style element.
*/
private function process_style_element( DOMElement $element ) {
- if ( $element->hasAttribute( 'amp-keyframes' ) ) {
- $validity = $this->validate_amp_keyframe( $element );
- if ( is_wp_error( $validity ) ) {
- $this->remove_invalid_child( $element, array(
- 'message' => $validity->get_error_message(),
- ) );
- }
- return;
- }
- $rules = trim( $element->textContent );
- $rules = $this->remove_illegal_css( $rules, $element );
+ // @todo Any @keyframes rules could be removed from amp-custom and instead added to amp-keyframes.
+ $is_keyframes = $element->hasAttribute( 'amp-keyframes' );
+ $stylesheet = trim( $element->textContent );
+ $cdata_spec = $is_keyframes ? $this->style_keyframes_cdata_spec : $this->style_custom_cdata_spec;
+ if ( $stylesheet ) {
- // Remove if surpasses max size.
- $length = strlen( $rules );
- if ( $this->current_custom_size + $length > $this->custom_max_size ) {
- $this->remove_invalid_child( $element, array(
- 'message' => __( 'Too much CSS enqueued.', 'amp' ),
+ $stylesheet = $this->process_stylesheet( $stylesheet, $element, array(
+ 'allowed_at_rules' => $cdata_spec['css_spec']['allowed_at_rules'],
+ 'property_whitelist' => $cdata_spec['css_spec']['allowed_declarations'],
+ 'validate_keyframes' => $cdata_spec['css_spec']['validate_keyframes'],
) );
- return;
- }
- $this->stylesheets[ md5( $rules ) ] = $rules;
- $this->current_custom_size += $length;
+ $pending_stylesheet = array(
+ 'keyframes' => $is_keyframes,
+ 'stylesheet' => $stylesheet,
+ 'node' => $element,
+ );
+ if ( ! empty( $this->args['validation_error_callback'] ) ) {
+ $pending_stylesheet['sources'] = AMP_Validation_Utils::locate_sources( $element ); // Needed because node is removed below.
+ }
+ $this->pending_stylesheets[] = $pending_stylesheet;
+ }
if ( $element->hasAttribute( 'amp-custom' ) ) {
if ( ! $this->amp_custom_style_element ) {
@@ -340,7 +399,7 @@ private function process_link_element( DOMElement $element ) {
return;
}
- $css_file_path = $this->get_validated_css_file_path( $href );
+ $css_file_path = $this->get_validated_url_file_path( $href, array( 'css', 'less', 'scss', 'sass' ) );
if ( is_wp_error( $css_file_path ) ) {
$this->remove_invalid_child( $element, array(
'message' => $css_file_path->get_error_message(),
@@ -349,90 +408,643 @@ private function process_link_element( DOMElement $element ) {
}
// Load the CSS from the filesystem.
- $rules = "\n/* $href */\n";
- $rules .= file_get_contents( $css_file_path ); // phpcs:ignore -- It's a local filesystem path not a remote request.
-
- $rules = $this->remove_illegal_css( $rules, $element );
+ $stylesheet = file_get_contents( $css_file_path ); // phpcs:ignore -- It's a local filesystem path not a remote request.
+ if ( false === $stylesheet ) {
+ $this->remove_invalid_child( $element, array(
+ 'message' => __( 'Unable to load stylesheet from filesystem.', 'amp' ),
+ ) );
+ return;
+ }
+ // Honor the link's media attribute.
$media = $element->getAttribute( 'media' );
if ( $media && 'all' !== $media ) {
- $rules = sprintf( '@media %s { %s }', $media, $rules );
+ $stylesheet = sprintf( '@media %s { %s }', $media, $stylesheet );
}
- // Remove if surpasses max size.
- $length = strlen( $rules );
- if ( $this->current_custom_size + $length > $this->custom_max_size ) {
- $this->remove_invalid_child( $element, array(
- 'message' => __( 'Too much CSS enqueued.', 'amp' ),
- ) );
- return;
+ $stylesheet = $this->process_stylesheet( $stylesheet, $element, array(
+ 'allowed_at_rules' => $this->style_custom_cdata_spec['css_spec']['allowed_at_rules'],
+ 'property_whitelist' => $this->style_custom_cdata_spec['css_spec']['allowed_declarations'],
+ 'stylesheet_url' => $href,
+ 'stylesheet_path' => $css_file_path,
+ ) );
+
+ $pending_stylesheet = array(
+ 'keyframes' => false,
+ 'stylesheet' => $stylesheet,
+ 'node' => $element,
+ );
+ if ( ! empty( $this->args['validation_error_callback'] ) ) {
+ $pending_stylesheet['sources'] = AMP_Validation_Utils::locate_sources( $element ); // Needed because node is removed below.
}
-
- $this->current_custom_size += $length;
- $this->stylesheets[ $href ] = $rules;
+ $this->pending_stylesheets[] = $pending_stylesheet;
// Remove now that styles have been processed.
$element->parentNode->removeChild( $element );
}
/**
- * Remove illegal CSS from the stylesheet.
+ * Process stylesheet.
*
- * @since 0.7
+ * Sanitized invalid CSS properties and rules, removes rules which do not
+ * apply to the current document, and compresses the CSS to remove whitespace and comments.
+ *
+ * @since 1.0
*
- * @todo This needs proper CSS parser and to take an alternative approach to removing !important by extracting
- * the rule into a separate style rule with a very specific selector.
- * @param string $stylesheet Stylesheet.
- * @param DOMElement $element Element where the stylesheet came from.
- * @return string Scrubbed stylesheet.
+ * @param string $stylesheet Stylesheet.
+ * @param DOMElement|DOMAttr $node Element (link/style) or style attribute where the stylesheet came from.
+ * @param array $options {
+ * Options.
+ *
+ * @type bool $class_selector_tree_shaking Whether to perform tree shaking to delete rules that reference class names not extant in the current document.
+ * @type string[] $property_whitelist Exclusively-allowed properties.
+ * @type string[] $property_blacklist Disallowed properties.
+ * @type bool $convert_width_to_max_width Convert width to max-width.
+ * @type string $stylesheet_url Original URL for stylesheet when originating via link (or @import?).
+ * @type string $stylesheet_path Original filesystem path for stylesheet when originating via link (or @import?).
+ * @type array $allowed_at_rules Allowed @-rules.
+ * @type bool $validate_keyframes Whether keyframes should be validated.
+ * }
+ * @return array Processed stylesheet parts.
*/
- private function remove_illegal_css( $stylesheet, $element ) {
- $stylesheet = preg_replace( '/\s*!important/', '', $stylesheet, -1, $important_count ); // Note this has to also replace inside comments to be valid.
- if ( $important_count > 0 && ! empty( $this->args['validation_error_callback'] ) ) {
- call_user_func( $this->args['validation_error_callback'], array(
- 'code' => 'css_important_removed',
- 'node' => $element,
- ) );
+ private function process_stylesheet( $stylesheet, $node, $options = array() ) {
+ $cache_impacting_options = wp_array_slice_assoc(
+ $options,
+ array( 'property_whitelist', 'property_blacklist', 'convert_width_to_max_width', 'stylesheet_url', 'allowed_at_rules' )
+ );
+
+ $cache_key = md5( $stylesheet . serialize( $cache_impacting_options ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
+
+ $cache_group = 'amp-parsed-stylesheet-v1';
+ if ( wp_using_ext_object_cache() ) {
+ $parsed = wp_cache_get( $cache_key, $cache_group );
+ } else {
+ $parsed = get_transient( $cache_key . $cache_group );
}
- $stylesheet = preg_replace( '/overflow(-[xy])?\s*:\s*(auto|scroll)\s*;?\s*/', '', $stylesheet, -1, $overlow_count );
- if ( $overlow_count > 0 && ! empty( $this->args['validation_error_callback'] ) ) {
- call_user_func( $this->args['validation_error_callback'], array(
- 'code' => 'css_overflow_property_removed',
- 'node' => $element,
- ) );
+ if ( ! $parsed || ! isset( $parsed['stylesheet'] ) || ! is_array( $parsed['stylesheet'] ) ) {
+ $parsed = $this->parse_stylesheet( $stylesheet, $options );
+ if ( wp_using_ext_object_cache() ) {
+ wp_cache_set( $cache_key, $parsed, $cache_group );
+ } else {
+ // The expiration is to ensure transient doesn't stick around forever since no LRU flushing like with external object cache.
+ set_transient( $cache_key . $cache_group, $parsed, MONTH_IN_SECONDS );
+ }
}
- return $stylesheet;
+
+ if ( ! empty( $this->args['validation_error_callback'] ) && ! empty( $parsed['validation_errors'] ) ) {
+ foreach ( $parsed['validation_errors'] as $validation_error ) {
+ call_user_func( $this->args['validation_error_callback'], array_merge( $validation_error, compact( 'node' ) ) );
+ }
+ }
+
+ return $parsed['stylesheet'];
}
/**
- * Validate amp-keyframe style.
+ * Parse stylesheet.
*
- * @since 0.7
- * @link https://github.com/ampproject/amphtml/blob/b685a0780a7f59313666225478b2b79b463bcd0b/validator/validator-main.protoascii#L1002-L1043
+ * @since 1.0
+ *
+ * @param string $stylesheet_string Stylesheet.
+ * @param array $options Options. See definition in \AMP_Style_Sanitizer::process_stylesheet().
+ * @return array {
+ * Parsed stylesheet.
+ *
+ * @type array $stylesheet Stylesheet parts, where arrays are tuples for declaration blocks.
+ * @type array $validation_errors Validation errors.
+ * }
+ */
+ private function parse_stylesheet( $stylesheet_string, $options = array() ) {
+ $start_time = microtime( true );
+
+ $options = array_merge(
+ array(
+ 'allowed_at_rules' => array(),
+ 'convert_width_to_max_width' => false,
+ 'property_blacklist' => array(
+ // See .
+ 'behavior',
+ '-moz-binding',
+ ),
+ 'property_whitelist' => array(),
+ 'validate_keyframes' => false,
+ 'stylesheet_url' => null,
+ 'stylesheet_path' => null,
+ ),
+ $options
+ );
+
+ $stylesheet = array();
+ $validation_errors = array();
+ try {
+ $parser_settings = Sabberworm\CSS\Settings::create();
+ $css_parser = new Sabberworm\CSS\Parser( $stylesheet_string, $parser_settings );
+ $css_document = $css_parser->parse();
+
+ if ( ! empty( $options['stylesheet_url'] ) ) {
+ $this->real_path_urls(
+ array_filter(
+ $css_document->getAllValues(),
+ function ( $value ) {
+ return $value instanceof URL;
+ }
+ ),
+ $options['stylesheet_url']
+ );
+ }
+
+ $validation_errors = $this->process_css_list( $css_document, $options );
+
+ $output_format = Sabberworm\CSS\OutputFormat::createCompact();
+
+ $before_declaration_block = '/*AMP_WP_BEFORE_DECLARATION_BLOCK*/';
+ $between_selectors = '/*AMP_WP_BETWEEN_SELECTORS*/';
+ $after_declaration_block_selectors = '/*AMP_WP_BEFORE_DECLARATION_SELECTORS*/';
+ $after_declaration_block = '/*AMP_WP_AFTER_DECLARATION*/';
+
+ $output_format->set( 'BeforeDeclarationBlock', $before_declaration_block );
+ $output_format->set( 'SpaceBeforeSelectorSeparator', $between_selectors );
+ $output_format->set( 'AfterDeclarationBlockSelectors', $after_declaration_block_selectors );
+ $output_format->set( 'AfterDeclarationBlock', $after_declaration_block );
+
+ $stylesheet_string = $css_document->render( $output_format );
+
+ $pattern = '#';
+ $pattern .= '(' . preg_quote( $before_declaration_block, '#' ) . ')';
+ $pattern .= '(.+?)';
+ $pattern .= preg_quote( $after_declaration_block_selectors, '#' );
+ $pattern .= '(.+?)';
+ $pattern .= preg_quote( $after_declaration_block, '#' );
+ $pattern .= '#s';
+
+ $split_stylesheet = preg_split( $pattern, $stylesheet_string, -1, PREG_SPLIT_DELIM_CAPTURE );
+ $length = count( $split_stylesheet );
+ for ( $i = 0; $i < $length; $i++ ) {
+ if ( $before_declaration_block === $split_stylesheet[ $i ] ) {
+ $selectors = explode( $between_selectors . ',', $split_stylesheet[ ++$i ] );
+ $declaration = $split_stylesheet[ ++$i ];
+
+ $selectors_parsed = array();
+ foreach ( $selectors as $selector ) {
+ $classes = array();
+
+ // Remove :not() to eliminate false negatives, such as with `body:not(.title-tagline-hidden) .site-branding-text`.
+ $reduced_selector = preg_replace( '/:not\(.+?\)/', '', $selector );
+
+ // Remove attribute selectors to eliminate false negative, such as with `.social-navigation a[href*="example.com"]:before`.
+ $reduced_selector = preg_replace( '/\[\w.*?\]/', '', $reduced_selector );
+
+ if ( preg_match_all( '/(?<=\.)([a-zA-Z0-9_-]+)/', $reduced_selector, $matches ) ) {
+ $classes = $matches[0];
+ }
+ $selectors_parsed[ $selector ] = $classes;
+ }
+
+ $stylesheet[] = array(
+ $selectors_parsed,
+ $declaration,
+ );
+ } else {
+ $stylesheet[] = $split_stylesheet[ $i ];
+ }
+ }
+ } catch ( Exception $exception ) {
+ $validation_errors[] = array(
+ 'code' => 'css_parse_error',
+ 'message' => $exception->getMessage(),
+ );
+ }
+
+ $this->parse_css_duration += ( microtime( true ) - $start_time );
+
+ return compact( 'stylesheet', 'validation_errors' );
+ }
+
+ /**
+ * Process CSS list.
+ *
+ * @since 1.0
+ *
+ * @param CSSList $css_list CSS List.
+ * @param array $options Options.
+ * @return array Validation errors.
+ */
+ private function process_css_list( CSSList $css_list, $options ) {
+ $validation_errors = array();
+
+ foreach ( $css_list->getContents() as $css_item ) {
+ if ( $css_item instanceof DeclarationBlock && empty( $options['validate_keyframes'] ) ) {
+ $validation_errors = array_merge(
+ $validation_errors,
+ $this->process_css_declaration_block( $css_item, $css_list, $options )
+ );
+ } elseif ( $css_item instanceof AtRuleBlockList ) {
+ if ( in_array( $css_item->atRuleName(), $options['allowed_at_rules'], true ) ) {
+ $validation_errors = array_merge(
+ $validation_errors,
+ $this->process_css_list( $css_item, $options )
+ );
+ } else {
+ $validation_errors[] = array(
+ 'code' => 'illegal_css_at_rule',
+ /* translators: %s is the CSS at-rule name. */
+ 'message' => sprintf( __( 'CSS @%s rules are currently disallowed.', 'amp' ), $css_item->atRuleName() ),
+ );
+ $css_list->remove( $css_item );
+ }
+ } elseif ( $css_item instanceof Import ) {
+ $validation_errors[] = array(
+ 'code' => 'illegal_css_import_rule',
+ 'message' => __( 'CSS @import is currently disallowed.', 'amp' ),
+ );
+ $css_list->remove( $css_item );
+ } elseif ( $css_item instanceof AtRuleSet ) {
+ if ( in_array( $css_item->atRuleName(), $options['allowed_at_rules'], true ) ) {
+ $validation_errors = array_merge(
+ $validation_errors,
+ $this->process_css_declaration_block( $css_item, $css_list, $options )
+ );
+ } else {
+ $validation_errors[] = array(
+ 'code' => 'illegal_css_at_rule',
+ /* translators: %s is the CSS at-rule name. */
+ 'message' => sprintf( __( 'CSS @%s rules are currently disallowed.', 'amp' ), $css_item->atRuleName() ),
+ );
+ $css_list->remove( $css_item );
+ }
+ } elseif ( $css_item instanceof KeyFrame ) {
+ if ( in_array( 'keyframes', $options['allowed_at_rules'], true ) ) {
+ $validation_errors = array_merge(
+ $validation_errors,
+ $this->process_css_keyframes( $css_item, $options )
+ );
+ } else {
+ $validation_errors[] = array(
+ 'code' => 'illegal_css_at_rule',
+ /* translators: %s is the CSS at-rule name. */
+ 'message' => sprintf( __( 'CSS @%s rules are currently disallowed.', 'amp' ), $css_item->atRuleName() ),
+ );
+ }
+ } elseif ( $css_item instanceof AtRule ) {
+ $validation_errors[] = array(
+ 'code' => 'illegal_css_at_rule',
+ /* translators: %s is the CSS at-rule name. */
+ 'message' => sprintf( __( 'CSS @%s rules are currently disallowed.', 'amp' ), $css_item->atRuleName() ),
+ );
+ $css_list->remove( $css_item );
+ } else {
+ $validation_errors[] = array(
+ 'code' => 'unrecognized_css',
+ 'message' => __( 'Unrecognized CSS removed.', 'amp' ),
+ );
+ $css_list->remove( $css_item );
+ }
+ }
+ return $validation_errors;
+ }
+
+ /**
+ * Convert URLs in to non-relative real-paths.
+ *
+ * @param URL[] $urls URLs.
+ * @param string $stylesheet_url Stylesheet URL.
+ */
+ private function real_path_urls( $urls, $stylesheet_url ) {
+ $base_url = preg_replace( ':[^/]+(\?.*)?(#.*)?$:', '', $stylesheet_url );
+ if ( empty( $base_url ) ) {
+ return;
+ }
+ foreach ( $urls as $url ) {
+ $url_string = $url->getURL()->getString();
+ if ( 'data:' === substr( $url_string, 0, 5 ) ) {
+ continue;
+ }
+
+ $parsed_url = wp_parse_url( $url_string );
+ if ( ! empty( $parsed_url['host'] ) || empty( $parsed_url['path'] ) || '/' === substr( $parsed_url['path'], 0, 1 ) ) {
+ continue;
+ }
+
+ $relative_url = preg_replace( '#^\./#', '', $url->getURL()->getString() );
+
+ $real_url = $base_url . $relative_url;
+ do {
+ $real_url = preg_replace( '#[^/]+/../#', '', $real_url, -1, $count );
+ } while ( 0 !== $count );
+
+ $url->getURL()->setString( $real_url );
+ }
+ }
+
+ /**
+ * Process CSS rule set.
+ *
+ * @since 1.0
+ * @link https://www.ampproject.org/docs/design/responsive/style_pages#disallowed-styles
+ * @link https://www.ampproject.org/docs/design/responsive/style_pages#restricted-styles
*
- * @param DOMElement $style Style element.
- * @return true|WP_Error Validity.
+ * @param RuleSet $ruleset Ruleset.
+ * @param CSSList $css_list CSS List.
+ * @param array $options Options.
+ *
+ * @return array Validation errors.
*/
- private function validate_amp_keyframe( $style ) {
- if ( 'body' !== $style->parentNode->nodeName ) {
- return new WP_Error( 'mandatory_body_child', __( 'amp-keyframes is not child of body element.', 'amp' ) );
+ private function process_css_declaration_block( RuleSet $ruleset, CSSList $css_list, $options ) {
+ $validation_errors = array();
+
+ // Remove disallowed properties.
+ if ( ! empty( $options['property_whitelist'] ) ) {
+ $properties = $ruleset->getRules();
+ foreach ( $properties as $property ) {
+ $vendorless_property_name = preg_replace( '/^-\w+-/', '', $property->getRule() );
+ if ( ! in_array( $vendorless_property_name, $options['property_whitelist'], true ) ) {
+ $validation_errors[] = array(
+ 'code' => 'illegal_css_property',
+ 'property_name' => $property->getRule(),
+ 'property_value' => $property->getValue(),
+ );
+ $ruleset->removeRule( $property->getRule() );
+ }
+ }
+ } else {
+ foreach ( $options['property_blacklist'] as $illegal_property_name ) {
+ $properties = $ruleset->getRules( $illegal_property_name );
+ foreach ( $properties as $property ) {
+ $validation_errors[] = array(
+ 'code' => 'illegal_css_property',
+ 'property_name' => $property->getRule(),
+ 'property_value' => $property->getValue(),
+ );
+ $ruleset->removeRule( $property->getRule() );
+ }
+ }
}
- if ( $this->keyframes_max_size && strlen( $style->textContent ) > $this->keyframes_max_size ) {
- return new WP_Error( 'max_bytes', __( 'amp-keyframes is too large', 'amp' ) );
+ if ( $ruleset instanceof AtRuleSet && 'font-face' === $ruleset->atRuleName() ) {
+ $this->process_font_face_at_rule( $ruleset, $options );
}
- // This logic could be in AMP_Tag_And_Attribute_Sanitizer, but since it only applies to amp-keyframes it seems unnecessary.
- $next_sibling = $style->nextSibling;
- while ( $next_sibling ) {
- if ( $next_sibling instanceof DOMElement ) {
- return new WP_Error( 'mandatory_last_child', __( 'amp-keyframes is not last element in body.', 'amp' ) );
+ $validation_errors = array_merge(
+ $validation_errors,
+ $this->transform_important_qualifiers( $ruleset, $css_list )
+ );
+
+ // Convert width to max-width when requested. See .
+ if ( $options['convert_width_to_max_width'] ) {
+ $properties = $ruleset->getRules( 'width' );
+ foreach ( $properties as $property ) {
+ $width_property = new Rule( 'max-width' );
+ $width_property->setValue( $property->getValue() );
+ $ruleset->removeRule( $property );
+ $ruleset->addRule( $width_property, $property );
}
- $next_sibling = $next_sibling->nextSibling;
}
- // @todo Also add validation of the CSS spec itself.
- return true;
+ // Remove the ruleset if it is now empty.
+ if ( 0 === count( $ruleset->getRules() ) ) {
+ $css_list->remove( $ruleset );
+ }
+ // @todo Delete rules with selectors for -amphtml- class and i-amphtml- tags.
+ return $validation_errors;
+ }
+
+ /**
+ * Process @font-face by making src URLs non-relative and converting data: URLs into (assumed) file URLs.
+ *
+ * @since 1.0
+ *
+ * @param AtRuleSet $ruleset Ruleset for @font-face.
+ * @param array $options Options.
+ */
+ private function process_font_face_at_rule( AtRuleSet $ruleset, $options ) {
+ $src_properties = $ruleset->getRules( 'src' );
+ if ( empty( $src_properties ) ) {
+ return;
+ }
+
+ foreach ( $src_properties as $src_property ) {
+ $value = $src_property->getValue();
+ if ( ! ( $value instanceof RuleValueList ) ) {
+ continue;
+ }
+
+ /*
+ * The CSS Parser parses a src such as:
+ *
+ * url(data:application/font-woff;...) format('woff'),
+ * url('Genericons.ttf') format('truetype'),
+ * url('Genericons.svg#genericonsregular') format('svg')
+ *
+ * As a list of components consisting of:
+ *
+ * URL,
+ * RuleValueList( CSSFunction, URL ),
+ * RuleValueList( CSSFunction, URL ),
+ * CSSFunction
+ *
+ * Clearly the components here are not logically grouped. So the first step is to fix the order.
+ */
+ $sources = array();
+ foreach ( $value->getListComponents() as $component ) {
+ if ( $component instanceof RuleValueList ) {
+ $subcomponents = $component->getListComponents();
+ $subcomponent = array_shift( $subcomponents );
+ if ( $subcomponent ) {
+ if ( empty( $sources ) ) {
+ $sources[] = array( $subcomponent );
+ } else {
+ $sources[ count( $sources ) - 1 ][] = $subcomponent;
+ }
+ }
+ foreach ( $subcomponents as $subcomponent ) {
+ $sources[] = array( $subcomponent );
+ }
+ } else {
+ if ( empty( $sources ) ) {
+ $sources[] = array( $component );
+ } else {
+ $sources[ count( $sources ) - 1 ][] = $component;
+ }
+ }
+ }
+
+ /**
+ * Source URL lists.
+ *
+ * @var URL[] $source_file_urls
+ * @var URL[] $source_data_urls
+ */
+ $source_file_urls = array();
+ $source_data_urls = array();
+ foreach ( $sources as $i => $source ) {
+ if ( $source[0] instanceof URL ) {
+ if ( 'data:' === substr( $source[0]->getURL()->getString(), 0, 5 ) ) {
+ $source_data_urls[ $i ] = $source[0];
+ } else {
+ $source_file_urls[ $i ] = $source[0];
+ }
+ }
+ }
+
+ // Convert data: URLs into regular URLs, assuming there will be a file present (e.g. woff fonts in core themes).
+ if ( empty( $source_file_urls ) ) {
+ continue;
+ }
+ $source_file_url = current( $source_file_urls );
+ foreach ( $source_data_urls as $i => $data_url ) {
+ $mime_type = strtok( substr( $data_url->getURL()->getString(), 5 ), ';' );
+ if ( ! $mime_type ) {
+ continue;
+ }
+ $extension = preg_replace( ':.+/(.+-)?:', '', $mime_type );
+ $guessed_url = preg_replace(
+ ':(?<=\.)\w+(\?.*)?(#.*)?$:', // Match the file extension in the URL.
+ $extension,
+ $source_file_url->getURL()->getString(),
+ 1,
+ $count
+ );
+ if ( 1 !== $count ) {
+ continue;
+ }
+
+ // Ensure file exists.
+ $path = $this->get_validated_url_file_path( $guessed_url );
+ if ( is_wp_error( $path ) ) {
+ continue;
+ }
+
+ $data_url->getURL()->setString( $guessed_url );
+ break;
+ }
+ }
+ }
+
+ /**
+ * Process CSS keyframes.
+ *
+ * @since 1.0
+ * @link https://www.ampproject.org/docs/design/responsive/style_pages#restricted-styles.
+ * @link https://github.com/ampproject/amphtml/blob/b685a0780a7f59313666225478b2b79b463bcd0b/validator/validator-main.protoascii#L1002-L1043
+ * @todo Tree shaking could be extended to keyframes, to omit a keyframe if it is not referenced by any rule.
+ *
+ * @param KeyFrame $css_list Ruleset.
+ * @param array $options Options.
+ * @return array Validation errors.
+ */
+ private function process_css_keyframes( KeyFrame $css_list, $options ) {
+ $validation_errors = array();
+ if ( ! empty( $options['property_whitelist'] ) ) {
+ foreach ( $css_list->getContents() as $rules ) {
+ if ( ! ( $rules instanceof DeclarationBlock ) ) {
+ $validation_errors[] = array(
+ 'code' => 'unrecognized_css',
+ 'message' => __( 'Unrecognized CSS removed.', 'amp' ),
+ );
+ $css_list->remove( $rules );
+ continue;
+ }
+
+ $validation_errors = array_merge(
+ $validation_errors,
+ $this->transform_important_qualifiers( $rules, $css_list )
+ );
+
+ $properties = $rules->getRules();
+ foreach ( $properties as $property ) {
+ $vendorless_property_name = preg_replace( '/^-\w+-/', '', $property->getRule() );
+ if ( ! in_array( $vendorless_property_name, $options['property_whitelist'], true ) ) {
+ $validation_errors[] = array(
+ 'code' => 'illegal_css_property',
+ 'property_name' => $property->getRule(),
+ 'property_value' => $property->getValue(),
+ );
+ $rules->removeRule( $property->getRule() );
+ }
+ }
+ }
+ }
+ return $validation_errors;
+ }
+
+ /**
+ * Replace !important qualifiers with more specific rules.
+ *
+ * @since 1.0
+ * @see https://www.npmjs.com/package/replace-important
+ * @see https://www.ampproject.org/docs/fundamentals/spec#important
+ *
+ * @param RuleSet|DeclarationBlock $ruleset Rule set.
+ * @param CSSList $css_list CSS List.
+ * @return array Validation errors.
+ */
+ private function transform_important_qualifiers( RuleSet $ruleset, CSSList $css_list ) {
+ $validation_errors = array();
+ $allow_transformation = (
+ $ruleset instanceof DeclarationBlock
+ &&
+ ! ( $css_list instanceof KeyFrame )
+ );
+
+ $properties = $ruleset->getRules();
+ $importants = array();
+ foreach ( $properties as $property ) {
+ if ( $property->getIsImportant() ) {
+ $property->setIsImportant( false );
+
+ // An !important doesn't make sense for rulesets that don't have selectors.
+ if ( $allow_transformation ) {
+ $importants[] = $property;
+ $ruleset->removeRule( $property->getRule() );
+ } else {
+ $validation_errors[] = array(
+ 'code' => 'illegal_css_important',
+ 'message' => __( 'Illegal CSS !important qualifier.', 'amp' ),
+ );
+ }
+ }
+ }
+ if ( ! $allow_transformation || empty( $importants ) ) {
+ return $validation_errors;
+ }
+
+ $important_ruleset = clone $ruleset;
+ $important_ruleset->setSelectors( array_map(
+ /**
+ * Modify selectors to be more specific to roughly match the effect of !important.
+ *
+ * @link https://github.com/ampproject/ampstart/blob/4c21d69afdd07b4c60cd190937bda09901955829/tools/replace-important/lib/index.js#L88-L109
+ *
+ * @param Selector $old_selector Original selector.
+ * @return Selector The new more-specific selector.
+ */
+ function( Selector $old_selector ) {
+ $specific = ':not(#_)'; // Here "_" is just a short single-char ID.
+
+ $selector_mod = str_repeat( $specific, floor( $old_selector->getSpecificity() / 100 ) );
+ if ( $old_selector->getSpecificity() % 100 > 0 ) {
+ $selector_mod .= $specific;
+ }
+ if ( $old_selector->getSpecificity() % 10 > 0 ) {
+ $selector_mod .= $specific;
+ }
+
+ $new_selector = $old_selector->getSelector();
+
+ // Amend the selector mod to the first element in selector if it is already the root; otherwise add new root ancestor.
+ if ( preg_match( '/^\s*(html|:root)\b/i', $new_selector, $matches ) ) {
+ $new_selector = substr( $new_selector, 0, strlen( $matches[0] ) ) . $selector_mod . substr( $new_selector, strlen( $matches[0] ) );
+ } else {
+ $new_selector = sprintf( ':root%s %s', $selector_mod, $new_selector );
+ }
+ return new Selector( $new_selector );
+ },
+ $ruleset->getSelectors()
+ ) );
+ $important_ruleset->setRules( $importants );
+ $css_list->append( $important_ruleset ); // @todo It would be preferable if the important ruleset were inserted adjacent to the original rule.
+
+ return $validation_errors;
}
/**
@@ -450,129 +1062,246 @@ private function validate_amp_keyframe( $style ) {
* @param DOMElement $element Node.
*/
private function collect_inline_styles( $element ) {
- $value = $element->getAttribute( 'style' );
- if ( ! $value ) {
+ $style_attribute = $element->getAttributeNode( 'style' );
+ if ( ! $style_attribute || ! trim( $style_attribute->nodeValue ) ) {
return;
}
- $class = $element->getAttribute( 'class' );
- $properties = $this->process_style( $value );
+ $class = 'amp-wp-' . substr( md5( $style_attribute->nodeValue ), 0, 7 );
+ $root = ':root' . str_repeat( ':not(#_)', 5 ); // @todo The correctness of using "5" should be validated.
+ $rule = sprintf( '%s .%s { %s }', $root, $class, $style_attribute->nodeValue );
- if ( ! empty( $properties ) ) {
- $class_name = $this->generate_class_name( $properties );
- $new_class = trim( $class . ' ' . $class_name );
+ $stylesheet = $this->process_stylesheet( $rule, $style_attribute, array(
+ 'convert_width_to_max_width' => true,
+ 'allowed_at_rules' => array(),
+ 'property_whitelist' => $this->style_custom_cdata_spec['css_spec']['allowed_declarations'],
+ ) );
- $selector = '.' . $class_name;
- $length = strlen( sprintf( '%s { %s }', $selector, join( '; ', $properties ) . ';' ) );
-
- if ( $this->current_custom_size + $length > $this->custom_max_size ) {
- $this->remove_invalid_attribute( $element, 'style', array(
- 'message' => __( 'Too much CSS.', 'amp' ),
- ) );
- return;
- }
+ if ( empty( $stylesheet ) ) {
+ $element->removeAttribute( 'style' );
+ return;
+ }
- $element->setAttribute( 'class', $new_class );
- $this->styles[ $selector ] = $properties;
+ $pending_stylesheet = array(
+ 'stylesheet' => $stylesheet,
+ 'node' => $element,
+ 'keyframes' => false,
+ );
+ if ( ! empty( $this->args['validation_error_callback'] ) ) {
+ $pending_stylesheet['sources'] = AMP_Validation_Utils::locate_sources( $element ); // Needed because node is removed below.
}
+
+ $this->pending_stylesheets[] = $pending_stylesheet;
+
$element->removeAttribute( 'style' );
+ if ( $element->hasAttribute( 'class' ) ) {
+ $element->setAttribute( 'class', $element->getAttribute( 'class' ) . ' ' . $class );
+ } else {
+ $element->setAttribute( 'class', $class );
+ }
}
/**
- * Sanitize and convert individual styles.
+ * Finalize stylesheets for style[amp-custom] and style[amp-keyframes] elements.
*
- * @since 0.4
+ * Concatenate all pending stylesheets, remove unused rules if necessary, and add to style elements in doc.
+ * Combine all amp-keyframe styles and add them to the end of the body.
*
- * @param string $css Style string.
- * @return array Style properties.
+ * @since 1.0
+ * @see https://www.ampproject.org/docs/fundamentals/spec#keyframes-stylesheet
*/
- private function process_style( $css ) {
+ private function finalize_styles() {
+
+ $stylesheet_sets = array(
+ 'custom' => array(
+ 'total_size' => 0,
+ 'cdata_spec' => $this->style_custom_cdata_spec,
+ 'pending_stylesheets' => array(),
+ 'final_stylesheets' => array(),
+ 'remove_unused_rules' => $this->args['remove_unused_rules'],
+ ),
+ 'keyframes' => array(
+ 'total_size' => 0,
+ 'cdata_spec' => $this->style_keyframes_cdata_spec,
+ 'pending_stylesheets' => array(),
+ 'final_stylesheets' => array(),
+ 'remove_unused_rules' => 'never', // Not relevant.
+ ),
+ );
- // Normalize whitespace.
- $css = str_replace( array( "\n", "\r", "\t" ), '', $css );
+ // Divide pending stylesheet between custom and keyframes, and calculate size of each.
+ while ( ! empty( $this->pending_stylesheets ) ) {
+ $pending_stylesheet = array_shift( $this->pending_stylesheets );
+
+ $set_name = ! empty( $pending_stylesheet['keyframes'] ) ? 'keyframes' : 'custom';
+ $size = 0;
+ foreach ( $pending_stylesheet['stylesheet'] as $part ) {
+ if ( is_string( $part ) ) {
+ $size += strlen( $part );
+ } elseif ( is_array( $part ) ) {
+ $size += strlen( implode( ',', array_keys( $part[0] ) ) ); // Selectors.
+ $size += strlen( $part[1] ); // Declaration block.
+ }
+ }
+ $stylesheet_sets[ $set_name ]['total_size'] += $size;
+ $stylesheet_sets[ $set_name ]['pending_stylesheets'][] = $pending_stylesheet;
+ }
- /*
- * Use preg_split to break up rules by `;` but only if the
- * semi-colon is not inside parens (like a data-encoded image).
- */
- $styles = preg_split( '/\s*;\s*(?![^(]*\))/', trim( $css, '; ' ) );
- $styles = array_filter( $styles );
+ // Process the pending stylesheets.
+ foreach ( array_keys( $stylesheet_sets ) as $set_name ) {
+ $stylesheet_sets[ $set_name ] = $this->finalize_stylesheet_set( $stylesheet_sets[ $set_name ] );
+ }
- // Normalize the order of the styles.
- sort( $styles );
+ $this->stylesheets = $stylesheet_sets['custom']['final_stylesheets'];
- $processed_styles = array();
+ // If we're not working with the document element (e.g. for legacy post templates) then there is nothing left to do.
+ if ( empty( $this->args['use_document_element'] ) ) {
+ return;
+ }
- // Normalize whitespace and filter rules.
- foreach ( $styles as $index => $rule ) {
- $tuple = preg_split( '/\s*:\s*/', $rule, 2 );
- if ( 2 !== count( $tuple ) ) {
- continue;
- }
+ // Add style[amp-custom] to document.
+ if ( ! empty( $stylesheet_sets['custom']['final_stylesheets'] ) ) {
- list( $property, $value ) = $this->filter_style( $tuple[0], $tuple[1] );
- if ( empty( $property ) || empty( $value ) ) {
- continue;
+ // Ensure style[amp-custom] is present in the document.
+ if ( ! $this->amp_custom_style_element ) {
+ $this->amp_custom_style_element = $this->dom->createElement( 'style' );
+ $this->amp_custom_style_element->setAttribute( 'amp-custom', '' );
+ $head = $this->dom->getElementsByTagName( 'head' )->item( 0 );
+ if ( ! $head ) {
+ $head = $this->dom->createElement( 'head' );
+ $this->dom->documentElement->insertBefore( $head, $this->dom->documentElement->firstChild );
+ }
+ $head->appendChild( $this->amp_custom_style_element );
}
- $processed_styles[ $index ] = "{$property}:{$value}";
+ $css = implode( '', $stylesheet_sets['custom']['final_stylesheets'] );
+
+ /*
+ * Let the style[amp-custom] be populated with the concatenated CSS.
+ * !important: Updating the contents of this style element by setting textContent is not
+ * reliable across PHP/libxml versions, so this is why the children are removed and the
+ * text node is then explicitly added containing the CSS.
+ */
+ while ( $this->amp_custom_style_element->firstChild ) {
+ $this->amp_custom_style_element->removeChild( $this->amp_custom_style_element->firstChild );
+ }
+ $this->amp_custom_style_element->appendChild( $this->dom->createTextNode( $css ) );
}
- return $processed_styles;
+ // Add style[amp-keyframes] to document.
+ if ( ! empty( $stylesheet_sets['keyframes']['final_stylesheets'] ) ) {
+ $body = $this->dom->getElementsByTagName( 'body' )->item( 0 );
+ if ( ! $body ) {
+ if ( ! empty( $this->args['validation_error_callback'] ) ) {
+ call_user_func( $this->args['validation_error_callback'], array(
+ 'code' => 'missing_body_element',
+ 'message' => __( 'amp-keyframes must be last child of body element.', 'amp' ),
+ ) );
+ }
+ } else {
+ $style_element = $this->dom->createElement( 'style' );
+ $style_element->setAttribute( 'amp-keyframes', '' );
+ $style_element->appendChild( $this->dom->createTextNode( implode( '', $stylesheet_sets['keyframes']['final_stylesheets'] ) ) );
+ $body->appendChild( $style_element );
+ }
+ }
}
/**
- * Filter individual CSS name/value pairs.
+ * Finalize a stylesheet set (amp-custom or amp-keyframes).
*
- * - Remove overflow if value is `auto` or `scroll`
- * - Change `width` to `max-width`
- * - Remove !important
+ * @since 1.0
*
- * @since 0.4
- *
- * @param string $property Property.
- * @param string $value Value.
- * @return array
+ * @param array $stylesheet_set Stylesheet set.
+ * @return array Finalized stylesheet set.
*/
- private function filter_style( $property, $value ) {
- /*
- * Remove overflow if value is `auto` or `scroll`; not allowed in AMP
- *
- * @todo This removal needs to be reported.
- * @see https://www.ampproject.org/docs/reference/spec.html#properties
- */
- if ( preg_match( '#^overflow(-[xy])?$#i', $property ) && preg_match( '#^(auto|scroll)$#i', $value ) ) {
- return array( false, false );
- }
+ private function finalize_stylesheet_set( $stylesheet_set ) {
+ $is_too_much_css = $stylesheet_set['total_size'] > $stylesheet_set['cdata_spec']['max_bytes'];
+ $should_tree_shake = (
+ 'always' === $stylesheet_set['remove_unused_rules'] || (
+ $is_too_much_css
+ &&
+ 'sometimes' === $stylesheet_set['remove_unused_rules']
+ )
+ );
- if ( 'width' === $property ) {
- $property = 'max-width';
+ if ( $is_too_much_css && $should_tree_shake && ! empty( $this->args['validation_error_callback'] ) ) {
+ call_user_func( $this->args['validation_error_callback'], array(
+ 'code' => 'removed_unused_css_rules',
+ 'message' => __( 'Too much CSS is enqueued and so seemingly irrelevant rules have been removed.', 'amp' ),
+ ) );
}
- /*
- * Remove `!important`; not allowed in AMP
- *
- * @todo This removal needs to be reported.
- */
- if ( false !== strpos( $value, 'important' ) ) {
- $value = preg_replace( '/\s*\!\s*important$/', '', $value );
+ $dynamic_selector_pattern = null;
+ if ( $should_tree_shake && ! empty( $this->args['dynamic_element_selectors'] ) ) {
+ $dynamic_selector_pattern = '#' . implode( '|', array_map(
+ function( $selector ) {
+ return preg_quote( $selector, '#' );
+ },
+ $this->args['dynamic_element_selectors']
+ ) ) . '#';
}
- return array( $property, $value );
- }
+ $final_size = 0;
+ foreach ( $stylesheet_set['pending_stylesheets'] as $pending_stylesheet ) {
+ $stylesheet = '';
+ foreach ( $pending_stylesheet['stylesheet'] as $stylesheet_part ) {
+ if ( is_string( $stylesheet_part ) ) {
+ $stylesheet .= $stylesheet_part;
+ } else {
+ list( $selectors_parsed, $declaration_block ) = $stylesheet_part;
+ if ( $should_tree_shake ) {
+ $selectors = array();
+ foreach ( $selectors_parsed as $selector => $class_names ) {
+ $should_include = (
+ ( $dynamic_selector_pattern && preg_match( $dynamic_selector_pattern, $selector ) )
+ ||
+ // If all class names are used in the doc.
+ 0 === count( array_diff( $class_names, $this->get_used_class_names() ) )
+ );
+ if ( $should_include ) {
+ $selectors[] = $selector;
+ }
+ }
+ } else {
+ $selectors = array_keys( $selectors_parsed );
+ }
+ if ( ! empty( $selectors ) ) {
+ $stylesheet .= implode( ',', $selectors ) . $declaration_block;
+ }
+ }
+ }
- /**
- * Generate a unique class name
- *
- * Use the md5() of the $data parameter
- *
- * @since 0.4
- *
- * @param string $data Data.
- * @return string Class name.
- */
- private function generate_class_name( $data ) {
- $string = maybe_serialize( $data );
- return 'amp-wp-inline-' . md5( $string );
+ // Skip considering stylesheet if an identical one has already been processed.
+ $hash = md5( $stylesheet );
+ if ( isset( $stylesheet_set['final_stylesheets'][ $hash ] ) ) {
+ continue;
+ }
+
+ // Report validation error if size is now too big.
+ $sheet_size = strlen( $stylesheet );
+ if ( $final_size + $sheet_size > $stylesheet_set['cdata_spec']['max_bytes'] ) {
+ if ( ! empty( $this->args['validation_error_callback'] ) ) {
+ $validation_error = array(
+ 'code' => 'excessive_css',
+ 'message' => sprintf(
+ /* translators: %d is the number of bytes over the limit */
+ __( 'Too much CSS output (by %d bytes).', 'amp' ),
+ ( $final_size + $sheet_size ) - $stylesheet_set['cdata_spec']['max_bytes']
+ ),
+ );
+ if ( isset( $pending_stylesheet['sources'] ) ) {
+ $validation_error['sources'] = $pending_stylesheet['sources'];
+ }
+ call_user_func( $this->args['validation_error_callback'], $validation_error );
+ }
+ } else {
+ $final_size += $sheet_size;
+
+ $stylesheet_set['final_stylesheets'][ $hash ] = $stylesheet;
+ }
+ }
+
+ return $stylesheet_set;
}
}
diff --git a/includes/templates/class-amp-content-sanitizer.php b/includes/templates/class-amp-content-sanitizer.php
index d7b75c28ec3..d9206b427f3 100644
--- a/includes/templates/class-amp-content-sanitizer.php
+++ b/includes/templates/class-amp-content-sanitizer.php
@@ -17,6 +17,7 @@ class AMP_Content_Sanitizer {
*
* @since 0.4.1
* @since 0.7 Passing return_styles=false in $global_args causes stylesheets to be returned instead of styles.
+ * @deprecated Since 1.0
*
* @param string $content HTML content string or DOM document.
* @param string[] $sanitizer_classes Sanitizer classes.
@@ -63,6 +64,7 @@ public static function sanitize_document( &$dom, $sanitizer_classes, $args ) {
$return_styles = ! empty( $args['return_styles'] );
unset( $args['return_styles'] );
foreach ( $sanitizer_classes as $sanitizer_class => $sanitizer_args ) {
+ $sanitize_class_start = microtime( true );
if ( ! class_exists( $sanitizer_class ) ) {
/* translators: %s is sanitizer class */
_doing_it_wrong( __METHOD__, sprintf( esc_html__( 'Sanitizer (%s) class does not exist', 'amp' ), esc_html( $sanitizer_class ) ), '0.4.1' );
@@ -90,6 +92,8 @@ public static function sanitize_document( &$dom, $sanitizer_classes, $args ) {
} else {
$stylesheets = array_merge( $stylesheets, $sanitizer->get_stylesheets() );
}
+
+ AMP_Response_Headers::send_server_timing( 'amp_sanitize', -$sanitize_class_start, $sanitizer_class );
}
return compact( 'scripts', 'styles', 'stylesheets' );
diff --git a/includes/templates/class-amp-content.php b/includes/templates/class-amp-content.php
index 06897b8edf1..2dc7890d0fe 100644
--- a/includes/templates/class-amp-content.php
+++ b/includes/templates/class-amp-content.php
@@ -34,10 +34,19 @@ class AMP_Content {
/**
* AMP styles.
*
+ * @deprecated
* @var array
*/
private $amp_styles = array();
+ /**
+ * AMP stylesheets.
+ *
+ * @since 1.0
+ * @var array
+ */
+ private $amp_stylesheets = array();
+
/**
* Args.
*
@@ -97,10 +106,22 @@ public function get_amp_scripts() {
/**
* Get AMP styles.
*
- * @return array
+ * @deprecated Since 1.0 in favor of the get_amp_stylesheets method.
+ * @return array Empty list.
*/
public function get_amp_styles() {
- return $this->amp_styles;
+ _deprecated_function( __METHOD__, '1.0', __CLASS__ . '::get_amp_stylesheets' );
+ return array();
+ }
+
+ /**
+ * Get AMP styles.
+ *
+ * @since 1.0
+ * @return array
+ */
+ public function get_amp_stylesheets() {
+ return $this->amp_stylesheets;
}
/**
@@ -130,12 +151,13 @@ private function add_scripts( $scripts ) {
}
/**
- * Add styles.
+ * Add stylesheets.
*
- * @param array $styles Styles.
+ * @since 1.0
+ * @param array $stylesheets Styles.
*/
- private function add_styles( $styles ) {
- $this->amp_styles = array_merge( $this->amp_styles, $styles );
+ private function add_stylesheets( $stylesheets ) {
+ $this->amp_stylesheets = array_merge( $this->amp_stylesheets, $stylesheets );
}
/**
@@ -179,14 +201,16 @@ private function unregister_embed_handlers( $embed_handlers ) {
*
* @see AMP_Content_Sanitizer::sanitize()
* @param string $content Content.
- * @return array Sanitized content.
+ * @return string Sanitized content.
*/
private function sanitize( $content ) {
- list( $sanitized_content, $scripts, $styles ) = AMP_Content_Sanitizer::sanitize( $content, $this->sanitizer_classes, $this->args );
+ $dom = AMP_DOM_Utils::get_dom_from_content( $content );
+
+ $results = AMP_Content_Sanitizer::sanitize_document( $dom, $this->sanitizer_classes, $this->args );
- $this->add_scripts( $scripts );
- $this->add_styles( $styles );
+ $this->add_scripts( $results['scripts'] );
+ $this->add_stylesheets( $results['stylesheets'] );
- return $sanitized_content;
+ return AMP_DOM_Utils::get_content_from_dom( $dom );
}
}
diff --git a/includes/templates/class-amp-post-template.php b/includes/templates/class-amp-post-template.php
index a917e7aaf10..d0d142d3d36 100644
--- a/includes/templates/class-amp-post-template.php
+++ b/includes/templates/class-amp-post-template.php
@@ -129,7 +129,8 @@ public function __construct( $post ) {
'merriweather' => 'https://fonts.googleapis.com/css?family=Merriweather:400,400italic,700,700italic',
),
- 'post_amp_styles' => array(),
+ 'post_amp_stylesheets' => array(),
+ 'post_amp_styles' => array(), // Deprecated.
'amp_analytics' => amp_add_custom_analytics(),
);
@@ -319,7 +320,7 @@ private function build_post_content() {
$this->add_data_by_key( 'post_amp_content', $amp_content->get_amp_content() );
$this->merge_data_for_key( 'amp_component_scripts', $amp_content->get_amp_scripts() );
- $this->merge_data_for_key( 'post_amp_styles', $amp_content->get_amp_styles() );
+ $this->add_data_by_key( 'post_amp_stylesheets', $amp_content->get_amp_stylesheets() );
}
/**
@@ -347,14 +348,17 @@ private function build_post_featured_image() {
$featured_image = get_post( $featured_id );
- list( $sanitized_html, $featured_scripts, $featured_styles ) = AMP_Content_Sanitizer::sanitize(
- $featured_html,
+ $dom = AMP_DOM_Utils::get_dom_from_content( $featured_html );
+ $assets = AMP_Content_Sanitizer::sanitize_document(
+ $dom,
array( 'AMP_Img_Sanitizer' => array() ),
array(
'content_max_width' => $this->get( 'content_max_width' ),
)
);
+ $sanitized_html = AMP_DOM_Utils::get_content_from_dom( $dom );
+
$this->add_data_by_key(
'featured_image', array(
'amp_html' => $sanitized_html,
@@ -362,12 +366,12 @@ private function build_post_featured_image() {
)
);
- if ( $featured_scripts ) {
- $this->merge_data_for_key( 'amp_component_scripts', $featured_scripts );
+ if ( $assets['scripts'] ) {
+ $this->merge_data_for_key( 'amp_component_scripts', $assets['scripts'] );
}
- if ( $featured_styles ) {
- $this->merge_data_for_key( 'post_amp_styles', $featured_styles );
+ if ( $assets['stylesheets'] ) {
+ $this->merge_data_for_key( 'post_amp_stylesheets', $assets['stylesheets'] );
}
}
diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php
index eff7c4cef3e..9982f4f3839 100644
--- a/includes/utils/class-amp-validation-utils.php
+++ b/includes/utils/class-amp-validation-utils.php
@@ -411,7 +411,9 @@ public static function add_validation_error( array $data ) {
$node = $data['node'];
unset( $data['node'] );
$data['node_name'] = $node->nodeName;
- $data['sources'] = self::locate_sources( $node );
+ if ( ! isset( $data['sources'] ) ) {
+ $data['sources'] = self::locate_sources( $node );
+ }
if ( $node->parentNode ) {
$data['parent_name'] = $node->parentNode->nodeName;
}
diff --git a/readme.md b/readme.md
index 161df3f30df..881a222ee0a 100644
--- a/readme.md
+++ b/readme.md
@@ -10,7 +10,7 @@ Enable Accelerated Mobile Pages (AMP) on your WordPress site.
**Tested up to:** 4.9
**Stable tag:** 0.6.0
**License:** [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html)
-**Requires PHP:** 5.3
+**Requires PHP:** 5.3.2
[![Build Status](https://travis-ci.org/Automattic/amp-wp.svg?branch=master)](https://travis-ci.org/Automattic/amp-wp) [![Built with Grunt](https://cdn.gruntjs.com/builtwith.svg)](http://gruntjs.com)
diff --git a/readme.txt b/readme.txt
index e741eb05b17..4b41f6f5a2d 100644
--- a/readme.txt
+++ b/readme.txt
@@ -6,7 +6,7 @@ Tested up to: 4.9
Stable tag: 0.6.0
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
-Requires PHP: 5.3
+Requires PHP: 5.3.2
Enable Accelerated Mobile Pages (AMP) on your WordPress site.
diff --git a/tests/test-amp-frontend-actions.php b/tests/test-amp-frontend-actions.php
index e2dd7d32241..31809e82e65 100644
--- a/tests/test-amp-frontend-actions.php
+++ b/tests/test-amp-frontend-actions.php
@@ -15,6 +15,7 @@ class Test_AMP_Frontend_Actions extends WP_UnitTestCase {
*/
public function setUp() {
parent::setUp();
+ require_once AMP__DIR__ . '/includes/amp-helper-functions.php';
amp_add_frontend_actions();
}
diff --git a/tests/test-amp-style-sanitizer.php b/tests/test-amp-style-sanitizer.php
index 7c708b42d29..5c7cd0a1afe 100644
--- a/tests/test-amp-style-sanitizer.php
+++ b/tests/test-amp-style-sanitizer.php
@@ -27,97 +27,133 @@ public function get_body_style_attribute_data() {
'span_one_style' => array(
'This is green.',
- 'This is green.',
+ 'This is green.',
array(
- '.amp-wp-inline-ad0e57ab02197f7023aa5b93bcf6c97e { color:#00ff00; }',
+ ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-bb01159{color:#0f0;}',
),
),
'span_one_style_bad_format' => array(
'This is green.',
- 'This is green.',
+ 'This is green.',
array(
- '.amp-wp-inline-ad0e57ab02197f7023aa5b93bcf6c97e { color:#00ff00; }',
+ ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-0837823{color:#0f0;}',
),
),
'span_two_styles_reversed' => array(
'This is green.',
- 'This is green.',
+ 'This is green.',
array(
- '.amp-wp-inline-58550689c128f3d396444313296e4c47 { background-color:#000; color:#00ff00; }',
+ ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-c71affe{color:#0f0;background-color:#000;}',
),
),
'width_to_max-width' => array(
'',
- '',
+ '',
array(
- '.amp-wp-inline-2676cd1bfa7e8feb4f0e0e8086ae9ce4 { max-width:300px; }',
+ ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-343bce0{max-width:300px;}',
),
),
'span_display_none' => array(
'Kses-banned properties are allowed since Kses will have already applied if user does not have unfiltered_html.',
- 'Kses-banned properties are allowed since Kses will have already applied if user does not have unfiltered_html.',
+ 'Kses-banned properties are allowed since Kses will have already applied if user does not have unfiltered_html.',
array(
- '.amp-wp-inline-0f1bf07c72fdf1784fff2e164d9dca98 { display:none; }',
+ ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-224b51a{display:none;}',
),
),
- 'div_amp_banned_style' => array(
- 'Scrollbars not allowed.',
- 'Scrollbars not allowed.',
- array(),
- ),
-
- '!important_not_allowed' => array(
- '!important not allowed.',
- '!important not allowed.',
+ '!important_is_ok' => array(
+ '!important is converted.',
+ '!important is converted.',
array(
- '.amp-wp-inline-b370df7c42957a3192cac40a8ddcff79 { margin:1px; }',
+ ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-6a75598{padding:1px;outline:3px;}:root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-6a75598{margin:2px;}',
),
),
- '!important_with_spaces_not_allowed' => array(
- '!important not allowed.',
- '!important not allowed.',
+ '!important_with_spaces_also_converted' => array(
+ '!important is converted.',
+ '!important is converted.',
array(
- '.amp-wp-inline-5b88d03e432f20476a218314084d3a05 { color:red; }',
+ ':root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-952600b{color:red;}',
),
),
- '!important_multiple_not_allowed' => array(
- '!important not allowed.',
- '!important not allowed.',
+ '!important_multiple_is_converted' => array(
+ '!important is converted.',
+ '!important is converted.',
array(
- '.amp-wp-inline-ef4329d562b6b3486a8a661df5c5280f { background:blue; color:red; }',
+ ':root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-1e2bfaa{color:red;background:blue;}',
),
),
'two_nodes' => array(
'This is red.',
- 'This is red.',
+ 'This is red.',
array(
- '.amp-wp-inline-ad0e57ab02197f7023aa5b93bcf6c97e { color:#00ff00; }',
- '.amp-wp-inline-f146f9bb819d875bbe5cf83e36368b44 { color:#ff0000; }',
+ ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-bb01159{color:#0f0;}',
+ ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-cc68ddc{color:#f00;}',
),
),
'existing_class_attribute' => array(
'',
- '',
+ '',
array(
- '.amp-wp-inline-3be9b2f79873ad78941ba2b3c03025a3 { background:#000; }',
+ ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-2864855{background:#000;}',
),
-
),
'inline_style_element_with_multiple_rules_containing_selectors_is_removed' => array(
- 'bold!
',
+ 'bold!
',
'bold!
',
array(
- 'div > span { font-weight:bold; font-style: italic; }',
+ 'div > span{font-style:italic;}@media screen and ( max-width: 640px ){div > span{font-style:normal;}:root:not(#_):not(#_) div > span{font-weight:normal;}}:root:not(#_):not(#_) div > span{font-weight:bold;}',
+ ),
+ ),
+
+ 'illegal_unsafe_properties' => array(
+ '',
+ '',
+ array(
+ 'button{font-weight:bold;}@media screen{button{font-weight:bold;}}',
+ ),
+ array( 'illegal_css_property', 'illegal_css_property', 'illegal_css_property', 'illegal_css_property' ),
+ ),
+
+ 'illegal_at_rule_in_style_attribute' => array(
+ 'Parse error.',
+ 'Parse error.',
+ array(),
+ array( 'css_parse_error' ),
+ ),
+
+ 'illegal_at_rules_removed' => array(
+ '',
+ '',
+ array(
+ 'body{color:black;}',
+ ),
+ array( 'illegal_css_at_rule', 'illegal_css_at_rule', 'illegal_css_at_rule', 'illegal_css_at_rule', 'illegal_css_at_rule' ),
+ ),
+
+ 'allowed_at_rules_retained' => array(
+ '',
+ '',
+ array(
+ '@media screen and ( max-width: 640px ){body{font-size:small;}}@font-face{font-family:"Open Sans";src:url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2");}@supports (display: grid){div{display:grid;}}@-moz-keyframes appear{from{opacity:0;}to{opacity:1;}}@keyframes appear{from{opacity:0;}to{opacity:1;}}',
+ ),
+ ),
+
+ 'selector_specificity' => array(
+ 'onetwothree
',
+ 'onetwothree
',
+ array(
+ ':root:not(#_) #child{color:red;}:root:not(#_):not(#_) #parent #child{color:pink;}:root:not(#_) .foo{color:blue;}:root:not(#_):not(#_) #me .foo{color:green;}',
+ ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-64b4fd4{color:yellow;}',
+ ':root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-ab79d9e{color:purple;}',
),
),
);
@@ -130,11 +166,19 @@ public function get_body_style_attribute_data() {
* @param string $source Source.
* @param string $expected_content Expected content.
* @param string $expected_stylesheets Expected stylesheets.
+ * @param array $expected_errors Expected error codes.
*/
- public function test_body_style_attribute_sanitizer( $source, $expected_content, $expected_stylesheets ) {
+ public function test_body_style_attribute_sanitizer( $source, $expected_content, $expected_stylesheets, $expected_errors = array() ) {
$dom = AMP_DOM_Utils::get_dom_from_content( $source );
- $sanitizer = new AMP_Style_Sanitizer( $dom );
+ $error_codes = array();
+ $args = array(
+ 'validation_error_callback' => function( $error ) use ( &$error_codes ) {
+ $error_codes[] = $error['code'];
+ },
+ );
+
+ $sanitizer = new AMP_Style_Sanitizer( $dom, $args );
$sanitizer->sanitize();
// Test content.
@@ -144,6 +188,8 @@ public function test_body_style_attribute_sanitizer( $source, $expected_content,
// Test stylesheet.
$this->assertEquals( $expected_stylesheets, array_values( $sanitizer->get_stylesheets() ) );
+
+ $this->assertEquals( $expected_errors, $error_codes );
}
/**
@@ -154,31 +200,75 @@ public function test_body_style_attribute_sanitizer( $source, $expected_content,
public function get_link_and_style_test_data() {
return array(
'multiple_amp_custom_and_other_styles' => array(
- '',
+ '',
array(
- 'b {color:red}',
- 'i {color:blue}',
- 'u {color:green}',
- 's {color:yellow}',
+ ':root:not(#_):not(#_) b{color:red;}',
+ 'i{color:blue;}',
+ 'u{color:green;}:root:not(#_):not(#_) u{text-decoration:underline;}',
+ 's{color:yellow;}',
),
+ array(),
),
'style_elements_with_link_elements' => array(
sprintf(
- '', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
+ '', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
includes_url( 'css/dashicons.css' )
),
array(
'strong.before-dashicon',
'.dashicons-dashboard:before',
'strong.after-dashicon',
- 's {color:yellow}',
+ ':root:not(#_):not(#_) s{color:yellow;}',
),
+ array(),
),
'style_with_no_head' => array(
- 'Not good!',
+ 'Not good!',
array(
'body{color:red;}',
),
+ array(),
+ ),
+ 'style_with_not_selectors' => array(
+ 'Hello
',
+ array(
+ 'body.foo:not(.bar) > p{color:blue;}body.foo:not(.bar) p:not(.baz){color:green;}body.foo p{color:yellow;}',
+ ),
+ array(),
+ ),
+ 'style_with_attribute_selectors' => array(
+ '',
+ array(
+ '.social-navigation a[href*="example.com"]{color:red;}',
+ ),
+ array(),
+ ),
+ 'style_on_root_element' => array(
+ 'Hi',
+ array(
+ 'html:not(#_):not(#_){background-color:blue;}',
+ ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-10b06ba{color:red;}',
+ ),
+ array(),
+ ),
+ 'styles_with_dynamic_elements' => array(
+ implode( '', array(
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ ' ',
+ '',
+ ) ),
+ array(
+ 'form [submit-success] b,div[submit-failure] b{color:green;}',
+ 'amp-live-list li .highlighted{background:yellow;}',
+ 'body amp-list .portland{color:blue;}',
+ ),
+ array(),
),
);
}
@@ -189,27 +279,154 @@ public function get_link_and_style_test_data() {
* @dataProvider get_link_and_style_test_data
* @param string $source Source.
* @param array $expected_stylesheets Expected stylesheets.
+ * @param array $expected_errors Expected error codes.
*/
- public function test_link_and_style_elements( $source, $expected_stylesheets ) {
+ public function test_link_and_style_elements( $source, $expected_stylesheets, $expected_errors = array() ) {
$dom = AMP_DOM_Utils::get_dom( $source );
- $sanitizer = new AMP_Style_Sanitizer( $dom, array(
- 'use_document_element' => true,
- ) );
+ $error_codes = array();
+ $args = array(
+ 'use_document_element' => true,
+ 'remove_unused_rules' => 'always',
+ 'validation_error_callback' => function( $error ) use ( &$error_codes ) {
+ $error_codes[] = $error['code'];
+ },
+ );
+
+ $sanitizer = new AMP_Style_Sanitizer( $dom, $args );
$sanitizer->sanitize();
- $whitelist_sanitizer = new AMP_Tag_And_Attribute_Sanitizer( $dom, array(
- 'use_document_element' => true,
- ) );
+ $whitelist_sanitizer = new AMP_Tag_And_Attribute_Sanitizer( $dom, $args );
$whitelist_sanitizer->sanitize();
$sanitized_html = AMP_DOM_Utils::get_content_from_dom_node( $dom, $dom->documentElement );
$actual_stylesheets = array_values( $sanitizer->get_stylesheets() );
$this->assertCount( count( $expected_stylesheets ), $actual_stylesheets );
foreach ( $expected_stylesheets as $i => $expected_stylesheet ) {
- $this->assertContains( $expected_stylesheet, $actual_stylesheets[ $i ] );
+ if ( false === strpos( $expected_stylesheet, '{' ) ) {
+ $this->assertContains( $expected_stylesheet, $actual_stylesheets[ $i ] );
+ } else {
+ $this->assertEquals( $expected_stylesheet, $actual_stylesheets[ $i ] );
+ }
$this->assertContains( $expected_stylesheet, $sanitized_html );
}
+
+ $this->assertEquals( $expected_errors, $error_codes );
+ }
+
+ /**
+ * Test handling of stylesheets with @font-face that have data: url source.
+ *
+ * Also confirm that class-based tree-shaking is working.
+ *
+ * @covers AMP_Style_Sanitizer::process_font_face_at_rule()
+ */
+ public function test_font_data_url_handling() {
+ $html = '';
+ $html .= ''; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
+ $html .= '';
+
+ // Test with tree-shaking.
+ $dom = AMP_DOM_Utils::get_dom( $html );
+ $error_codes = array();
+ $sanitizer = new AMP_Style_Sanitizer( $dom, array(
+ 'use_document_element' => true,
+ 'remove_unused_rules' => 'always',
+ 'validation_error_callback' => function( $error ) use ( &$error_codes ) {
+ $error_codes[] = $error['code'];
+ },
+ ) );
+ $sanitizer->sanitize();
+ $this->assertEquals( array(), $error_codes );
+ $actual_stylesheets = array_values( $sanitizer->get_stylesheets() );
+ $this->assertCount( 1, $actual_stylesheets );
+ $this->assertContains( 'dashicons.woff") format("woff")', $actual_stylesheets[0] );
+ $this->assertNotContains( 'data:application/font-woff;', $actual_stylesheets[0] );
+ $this->assertContains( '.dashicons{', $actual_stylesheets[0] );
+ $this->assertContains( '.dashicons-admin-appearance:before{', $actual_stylesheets[0] );
+ $this->assertNotContains( '.dashicons-format-chat:before', $actual_stylesheets[0] );
+
+ // Test with rule-removal not forced, since dashicons alone is not larger than 50KB.
+ $dom = AMP_DOM_Utils::get_dom( $html );
+ $error_codes = array();
+ $sanitizer = new AMP_Style_Sanitizer( $dom, array(
+ 'use_document_element' => true,
+ 'remove_unused_rules' => 'sometimes',
+ 'validation_error_callback' => function( $error ) use ( &$error_codes ) {
+ $error_codes[] = $error['code'];
+ },
+ ) );
+ $sanitizer->sanitize();
+ $this->assertEquals( array(), $error_codes );
+ $actual_stylesheets = array_values( $sanitizer->get_stylesheets() );
+ $this->assertContains( 'dashicons.woff") format("woff")', $actual_stylesheets[0] );
+ $this->assertNotContains( 'data:application/font-woff;', $actual_stylesheets[0] );
+ $this->assertContains( '.dashicons,.dashicons-before:before{', $actual_stylesheets[0] );
+ $this->assertContains( '.dashicons-admin-appearance:before{', $actual_stylesheets[0] );
+ $this->assertContains( '.dashicons-format-chat:before', $actual_stylesheets[0] );
+ }
+
+ /**
+ * Test that auto-removal is performed when remove_unused_rules=sometimes (the default), and that excessive CSS will be removed entirely.
+ *
+ * @covers AMP_Style_Sanitizer::finalize_stylesheet_set()
+ */
+ public function test_large_custom_css_and_rule_removal() {
+ $custom_max_size = null;
+ foreach ( AMP_Allowed_Tags_Generated::get_allowed_tag( 'style' ) as $spec_rule ) {
+ if ( isset( $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) && 'style amp-custom' === $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) {
+ $custom_max_size = $spec_rule[ AMP_Rule_Spec::CDATA ]['max_bytes'];
+ break;
+ }
+ }
+ $this->assertNotNull( $custom_max_size );
+
+ $html = '';
+ $html .= '';
+ $html .= '';
+ $html .= '...';
+ $dom = AMP_DOM_Utils::get_dom( $html );
+
+ $error_codes = array();
+ $sanitizer = new AMP_Style_Sanitizer( $dom, array(
+ 'use_document_element' => true,
+ 'validation_error_callback' => function( $error ) use ( &$error_codes ) {
+ $error_codes[] = $error['code'];
+ },
+ ) );
+ $sanitizer->sanitize();
+
+ $this->assertEquals(
+ array( '.b{color:blue;}' ),
+ array_values( $sanitizer->get_stylesheets() )
+ );
+
+ $this->assertEquals(
+ array( 'removed_unused_css_rules', 'excessive_css' ),
+ $error_codes
+ );
+ }
+
+ /**
+ * Test handling of stylesheets with relative background-image URLs.
+ *
+ * @covers AMP_Style_Sanitizer::real_path_urls()
+ */
+ public function test_relative_background_url_handling() {
+ $html = ''; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
+ $dom = AMP_DOM_Utils::get_dom( $html );
+
+ $sanitizer = new AMP_Style_Sanitizer( $dom, array(
+ 'use_document_element' => true,
+ ) );
+ $sanitizer->sanitize();
+ AMP_DOM_Utils::get_content_from_dom_node( $dom, $dom->documentElement );
+ $actual_stylesheets = array_values( $sanitizer->get_stylesheets() );
+ $this->assertCount( 1, $actual_stylesheets );
+ $stylesheet = $actual_stylesheets[0];
+
+ $this->assertNotContains( '../images/spinner', $stylesheet );
+ $this->assertContains( sprintf( '.spinner{background-image:url("%s");', admin_url( 'images/spinner-2x.gif' ) ), $stylesheet );
}
/**
@@ -218,28 +435,44 @@ public function test_link_and_style_elements( $source, $expected_stylesheets ) {
* @return array
*/
public function get_keyframe_data() {
- $keyframes_max_size = 10;
+ $keyframes_max_size = null;
foreach ( AMP_Allowed_Tags_Generated::get_allowed_tag( 'style' ) as $spec_rule ) {
if ( isset( $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) && 'style[amp-keyframes]' === $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) {
$keyframes_max_size = $spec_rule[ AMP_Rule_Spec::CDATA ]['max_bytes'];
break;
}
}
+ $this->assertNotNull( $keyframes_max_size );
return array(
'style_amp_keyframes' => array(
- '',
- null, // No Change.
+ '',
+ '',
+ array(),
),
'style_amp_keyframes_max_overflow' => array(
- '',
+ '',
'',
+ array( 'excessive_css' ),
),
'style_amp_keyframes_last_child' => array(
- ' as after',
- ' as after',
+ 'before between as after',
+ 'before between as after',
+ array(),
+ ),
+
+ 'blacklisted_and_whitelisted_keyframe_properties' => array(
+ '',
+ '',
+ array( 'illegal_css_property', 'illegal_css_property', 'illegal_css_property' ),
+ ),
+
+ 'style_amp_keyframes_with_disallowed_rules' => array(
+ '',
+ '',
+ array( 'unrecognized_css', 'illegal_css_important', 'illegal_css_at_rule' ),
),
);
}
@@ -250,15 +483,26 @@ public function get_keyframe_data() {
* @dataProvider get_keyframe_data
* @param string $source Markup to process.
* @param string $expected The markup to expect.
+ * @param array $expected_errors Expected error codes.
*/
- public function test_keyframe_sanitizer( $source, $expected = null ) {
- $expected = isset( $expected ) ? $expected : $source;
- $dom = AMP_DOM_Utils::get_dom_from_content( $source );
- $sanitizer = new AMP_Style_Sanitizer( $dom );
+ public function test_keyframe_sanitizer( $source, $expected = null, $expected_errors = array() ) {
+ $expected = isset( $expected ) ? $expected : $source;
+ $dom = AMP_DOM_Utils::get_dom_from_content( $source );
+ $error_codes = array();
+ $sanitizer = new AMP_Style_Sanitizer( $dom, array(
+ 'use_document_element' => true,
+ 'validation_error_callback' => function( $error ) use ( &$error_codes ) {
+ $error_codes[] = $error['code'];
+ },
+ ) );
$sanitizer->sanitize();
$content = AMP_DOM_Utils::get_content_from_dom( $dom );
+ $content = preg_replace( '#\s+(?=@keyframes)#', '', $content );
+ $content = preg_replace( '#\s+(?=)#', '', $content );
$content = preg_replace( '/(?<=>)\s+(?=<)/', '', $content );
$this->assertEquals( $expected, $content );
+
+ $this->assertEquals( $expected_errors, $error_codes );
}
/**
@@ -292,33 +536,34 @@ public function get_stylesheet_urls() {
admin_url( 'css/common.css' ),
ABSPATH . 'wp-admin/css/common.css',
),
- 'amp_css_bad_file_extension' => array(
+ 'amp_disallowed_file_extension' => array(
content_url( 'themes/twentyseventeen/index.php' ),
null,
- 'amp_css_bad_file_extension',
+ 'amp_disallowed_file_extension',
),
- 'amp_css_path_not_found' => array(
+ 'amp_file_path_not_found' => array(
content_url( 'themes/twentyseventeen/404.css' ),
null,
- 'amp_css_path_not_found',
+ 'amp_file_path_not_found',
),
);
}
/**
- * Tests get_validated_css_file_path.
+ * Tests get_validated_url_file_path.
*
* @dataProvider get_stylesheet_urls
- * @covers AMP_Style_Sanitizer::get_validated_css_file_path()
+ * @covers AMP_Style_Sanitizer::get_validated_url_file_path()
+ *
* @param string $source Source URL.
* @param string|null $expected Expected path or null if error.
* @param string $error_code Error code. Optional.
*/
- public function test_get_validated_css_file_path( $source, $expected, $error_code = null ) {
+ public function test_get_validated_url_file_path( $source, $expected, $error_code = null ) {
$dom = AMP_DOM_Utils::get_dom( '' );
$sanitizer = new AMP_Style_Sanitizer( $dom );
- $actual = $sanitizer->get_validated_css_file_path( $source );
+ $actual = $sanitizer->get_validated_url_file_path( $source, array( 'css' ) );
if ( isset( $error_code ) ) {
$this->assertInstanceOf( 'WP_Error', $actual );
$this->assertEquals( $error_code, $actual->get_error_code() );
diff --git a/tests/test-class-amp-response-headers.php b/tests/test-class-amp-response-headers.php
new file mode 100644
index 00000000000..a6408dd10e2
--- /dev/null
+++ b/tests/test-class-amp-response-headers.php
@@ -0,0 +1,116 @@
+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 e8e6cb0a9e4..670ad2fabdd 100644
--- a/tests/test-class-amp-theme-support.php
+++ b/tests/test-class-amp-theme-support.php
@@ -37,7 +37,7 @@ public function tearDown() {
if ( isset( $GLOBALS['wp_customize'] ) ) {
$GLOBALS['wp_customize']->stop_previewing_theme();
}
- AMP_Theme_Support::$headers_sent = array();
+ AMP_Response_Headers::$headers_sent = array();
}
/**
@@ -334,25 +334,6 @@ public function test_purge_amp_query_vars() {
// phpcs:enable WordPress.CSRF.NonceVerification.NoNonceVerification
}
- /**
- * Test send_header.
- *
- * @covers AMP_Theme_Support::send_header()
- */
- public function test_send_header() {
- $name = 'foo';
- $value = 'bar';
- $args = array(
- 'X-Example' => 'baz',
- );
- $default_args = array(
- 'replace' => true,
- 'status_code' => null,
- );
- AMP_Theme_Support::send_header( $name, $value, $args );
- $this->assertEquals( array_merge( compact( 'name', 'value' ), $args, $default_args ), reset( AMP_Theme_Support::$headers_sent ) );
- }
-
/**
* Test handle_xhr_request().
*
@@ -361,7 +342,7 @@ public function test_send_header() {
public function test_handle_xhr_request() {
AMP_Theme_Support::purge_amp_query_vars();
AMP_Theme_Support::handle_xhr_request();
- $this->assertEmpty( AMP_Theme_Support::$headers_sent );
+ $this->assertEmpty( AMP_Response_Headers::$headers_sent );
$_GET['_wp_amp_action_xhr_converted'] = '1';
@@ -370,14 +351,14 @@ public function test_handle_xhr_request() {
$_SERVER['REQUEST_METHOD'] = 'POST';
AMP_Theme_Support::purge_amp_query_vars();
AMP_Theme_Support::handle_xhr_request();
- $this->assertEmpty( AMP_Theme_Support::$headers_sent );
+ $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_Theme_Support::$headers_sent );
+ $this->assertCount( 1, AMP_Response_Headers::$headers_sent );
$this->assertEquals(
array(
'name' => 'AMP-Access-Control-Allow-Source-Origin',
@@ -385,7 +366,7 @@ public function test_handle_xhr_request() {
'replace' => true,
'status_code' => null,
),
- AMP_Theme_Support::$headers_sent[0]
+ 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' ) ) );
@@ -488,7 +469,7 @@ public function test_intercept_post_request_redirect() {
} );
// Test redirecting to full URL with HTTPS protocol.
- AMP_Theme_Support::$headers_sent = array();
+ AMP_Response_Headers::$headers_sent = array();
ob_start();
AMP_Theme_Support::intercept_post_request_redirect( $url );
$this->assertEquals( '{"success":true}', ob_get_clean() );
@@ -499,7 +480,7 @@ public function test_intercept_post_request_redirect() {
'replace' => true,
'status_code' => null,
),
- AMP_Theme_Support::$headers_sent
+ AMP_Response_Headers::$headers_sent
);
$this->assertContains(
array(
@@ -508,11 +489,11 @@ public function test_intercept_post_request_redirect() {
'replace' => true,
'status_code' => null,
),
- AMP_Theme_Support::$headers_sent
+ AMP_Response_Headers::$headers_sent
);
// Test redirecting to non-HTTPS URL.
- AMP_Theme_Support::$headers_sent = array();
+ AMP_Response_Headers::$headers_sent = array();
ob_start();
$url = home_url( '/', 'http' );
AMP_Theme_Support::intercept_post_request_redirect( $url );
@@ -524,7 +505,7 @@ public function test_intercept_post_request_redirect() {
'replace' => true,
'status_code' => null,
),
- AMP_Theme_Support::$headers_sent
+ AMP_Response_Headers::$headers_sent
);
$this->assertContains(
array(
@@ -533,11 +514,11 @@ public function test_intercept_post_request_redirect() {
'replace' => true,
'status_code' => null,
),
- AMP_Theme_Support::$headers_sent
+ AMP_Response_Headers::$headers_sent
);
// Test redirecting to host-less location.
- AMP_Theme_Support::$headers_sent = array();
+ AMP_Response_Headers::$headers_sent = array();
ob_start();
AMP_Theme_Support::intercept_post_request_redirect( '/new-location/' );
$this->assertEquals( '{"success":true}', ob_get_clean() );
@@ -548,11 +529,11 @@ public function test_intercept_post_request_redirect() {
'replace' => true,
'status_code' => null,
),
- AMP_Theme_Support::$headers_sent
+ AMP_Response_Headers::$headers_sent
);
// Test redirecting to scheme-less location.
- AMP_Theme_Support::$headers_sent = array();
+ 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 ) );
@@ -564,11 +545,11 @@ public function test_intercept_post_request_redirect() {
'replace' => true,
'status_code' => null,
),
- AMP_Theme_Support::$headers_sent
+ AMP_Response_Headers::$headers_sent
);
// Test redirecting to empty location.
- AMP_Theme_Support::$headers_sent = array();
+ AMP_Response_Headers::$headers_sent = array();
ob_start();
AMP_Theme_Support::intercept_post_request_redirect( '' );
$this->assertEquals( '{"success":true}', ob_get_clean() );
@@ -579,7 +560,7 @@ public function test_intercept_post_request_redirect() {
'replace' => true,
'status_code' => null,
),
- AMP_Theme_Support::$headers_sent
+ AMP_Response_Headers::$headers_sent
);
}
@@ -938,7 +919,6 @@ public function test_filter_customize_partial_render() {
$this->assertContains( 'assertNotContains( '', $sanitized_html ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
$this->assertContains( '', $sanitized_html ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
$this->assertContains( '', $sanitized_html ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript