Skip to content

Commit

Permalink
Use wp_unique_id-based block class names instead of uniqid (#6949)
Browse files Browse the repository at this point in the history
Co-authored-by: Piotr Delawski <piotr.delawski@gmail.com>
  • Loading branch information
westonruter and delawski committed Mar 18, 2022
1 parent 357706c commit 997b351
Show file tree
Hide file tree
Showing 9 changed files with 1,029 additions and 22 deletions.
1 change: 1 addition & 0 deletions .phpstorm.meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
'admin.validation_counts' => \AmpProject\AmpWP\Admin\ValidationCounts::class,
'amp_slug_customization_watcher' => \AmpProject\AmpWP\AmpSlugCustomizationWatcher::class,
'background_task_deactivator' => \AmpProject\AmpWP\BackgroundTask\BackgroundTaskDeactivator::class,
'block_uniqid_transformer' => \AmpProject\AmpWP\BlockUniqidTransformer::class,
'cli.command_namespace' => \AmpProject\AmpWP\CliCli\CommandNamespaceRegistration::class,
'cli.optimizer_command' => \AmpProject\AmpWP\CliCli\OptimizerCommand::class,
'cli.transformer_command' => \AmpProject\AmpWP\CliCli\TransformerCommand::class,
Expand Down
6 changes: 0 additions & 6 deletions includes/options/class-amp-options-manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -351,12 +351,6 @@ public static function validate_options( $new_options ) {
}
}

if ( array_key_exists( Option::DISABLE_CSS_TRANSIENT_CACHING, $new_options ) && true === $new_options[ Option::DISABLE_CSS_TRANSIENT_CACHING ] ) {
$options[ Option::DISABLE_CSS_TRANSIENT_CACHING ] = true;
} else {
unset( $options[ Option::DISABLE_CSS_TRANSIENT_CACHING ] );
}

/**
* Filter the options being updated, so services can handle the sanitization and validation of
* their respective options.
Expand Down
204 changes: 204 additions & 0 deletions includes/sanitizers/class-amp-block-uniqid-sanitizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<?php
/**
* Class AMP_Block_Uniqid_Sanitizer.
*
* @package AMP
*/

use AmpProject\Dom\Element;
use AmpProject\Html\Attribute;
use AmpProject\Html\Tag;
use AmpProject\Dom\Document;

/**
* Work around use of uniqid() in blocks which breaks parsed CSS caching.
*
* @link https://github.com/ampproject/amp-wp/issues/6925
* @link https://github.com/WordPress/gutenberg/issues/38889
*
* @since 2.2.2
* @internal
*/
class AMP_Block_Uniqid_Sanitizer extends AMP_Base_Sanitizer {

/**
* Prefixes of class names that should be transformed.
*
* @var string[]
*/
const KEY_PREFIXES = [
'wp-container-',
'wp-duotone-',
'wp-duotone-filter-',
'wp-elements-',
];

/**
* Class name pattern.
*
* @var string
*/
private $key_pattern;

/**
* The mapping between uniqid- and wp_unique_id-based class names.
*
* @var array
*/
private $key_mapping = [];

/**
* @param Document $dom DOM.
* @param array $args Args.
*/
public function __construct( $dom, $args = [] ) {
parent::__construct( $dom, $args );

$this->key_pattern = sprintf(
'/\b(?P<prefix>%s)(?P<uniqid>[0-9a-f]{13})\b/',
implode(
'|',
self::KEY_PREFIXES
)
);
}

/**
* Sanitize.
*/
public function sanitize() {
$elements = $this->dom->xpath->query(
sprintf(
'//*[ %s ]',
implode(
' or ',
array_map(
static function ( $class_name_prefix ) {
return sprintf(
'contains( @class, "%s" )',
$class_name_prefix
);
},
self::KEY_PREFIXES
)
)
)
);

$replaced_count = 0;
foreach ( $elements as $element ) {
if ( $this->transform_element_with_class_attribute( $element ) ) {
$replaced_count++;
}
}

if ( $replaced_count > 0 ) {
$this->transform_styles();
}
}

/**
* Transform element with class.
*
* @param Element $element Element.
*/
public function transform_element_with_class_attribute( Element $element ) {
$class_name = $element->getAttribute( Attribute::CLASS_ );

$count = 0;

$new_class_name = preg_replace_callback(
$this->key_pattern,
function ( $matches ) {
$old_key = $matches[0];

if ( ! isset( $this->key_mapping[ $old_key ] ) ) {
$this->key_mapping[ $old_key ] = self::unique_id( $matches['prefix'] );
}
$new_key = $this->key_mapping[ $old_key ];

if ( in_array( $matches['prefix'], [ 'wp-duotone-', 'wp-duotone-filter-' ], true ) ) {
$this->transform_duotone_filter( $old_key, $new_key );
}

return $new_key;
},
$class_name,
-1,
$count
);
if ( 0 === $count ) {
return false;
} else {
$element->setAttribute( Attribute::CLASS_, $new_class_name );
return true;
}
}

/**
* Transform duotone filter by updating its ID.
*
* @param string $old_key Old identifier.
* @param string $new_key New identifier.
*
* @return void
*/
public function transform_duotone_filter( $old_key, $new_key ) {
$svg_filter = $this->dom->getElementById( $old_key );
if ( $svg_filter instanceof Element && Tag::FILTER === $svg_filter->tagName ) {
$svg_filter->setAttribute( Attribute::ID, $new_key );
}
}

/**
* Transform styles.
*/
public function transform_styles() {
$styles = $this->dom->xpath->query(
sprintf(
'//style[ %s ]',
implode(
' or ',
array_map(
static function ( $key_prefix ) {
return sprintf(
'contains( text(), "%s" )',
$key_prefix
);
},
self::KEY_PREFIXES
)
)
)
);

foreach ( $styles as $style ) {
$style->textContent = str_replace(
array_keys( $this->key_mapping ),
array_values( $this->key_mapping ),
$style->textContent
);
}
}

/**
* Gets unique ID.
*
* This is a polyfill for WordPress <5.0.3.
*
* @see wp_unique_id()
*
* @param string $prefix Prefix for the returned ID.
* @return string Unique ID.
*/
private static function unique_id( $prefix = '' ) {
if ( function_exists( 'wp_unique_id' ) ) {
return wp_unique_id( $prefix );
} else {
// @codeCoverageIgnoreStart
static $id_counter = 0;
return $prefix . (string) ++$id_counter;
// @codeCoverageIgnoreEnd
}
}
}
1 change: 1 addition & 0 deletions src/AmpWpPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ final class AmpWpPlugin extends ServiceBasedPlugin {
'admin.amp_themes' => Admin\AmpThemes::class,
'amp_slug_customization_watcher' => AmpSlugCustomizationWatcher::class,
'background_task_deactivator' => BackgroundTaskDeactivator::class,
'block_uniqid_transformer' => BlockUniqidTransformer::class,
'cli.command_namespace' => Cli\CommandNamespaceRegistration::class,
'cli.optimizer_command' => Cli\OptimizerCommand::class,
'cli.transformer_command' => Cli\TransformerCommand::class,
Expand Down
117 changes: 110 additions & 7 deletions src/BackgroundTask/MonitorCssTransientCaching.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace AmpProject\AmpWP\BackgroundTask;

use AMP_Options_Manager;
use AmpProject\AmpWP\BlockUniqidTransformer;
use AmpProject\AmpWP\Option;
use DateTimeImmutable;
use DateTimeInterface;
Expand Down Expand Up @@ -54,13 +55,40 @@ final class MonitorCssTransientCaching extends RecurringBackgroundTask {
*/
const DEFAULT_SAMPLING_RANGE = 14;

/**
* @string
*/
const WP_VERSION = 'wp_version';

/**
* @string
*/
const GUTENBERG_VERSION = 'gutenberg_version';

/**
* @var BlockUniqidTransformer
*/
private $block_uniqid_transformer;

/**
* Constructor.
*
* @param BackgroundTaskDeactivator $background_task_deactivator Deactivator.
* @param BlockUniqidTransformer $block_uniqid_transformer Transformer.
*/
public function __construct( BackgroundTaskDeactivator $background_task_deactivator, BlockUniqidTransformer $block_uniqid_transformer ) {
parent::__construct( $background_task_deactivator );
$this->block_uniqid_transformer = $block_uniqid_transformer;
}

/**
* Register the service with the system.
*
* @return void
*/
public function register() {
add_action( 'amp_plugin_update', [ $this, 'handle_plugin_update' ] );
add_filter( 'amp_options_updating', [ $this, 'sanitize_disabled_option' ], 10, 2 );
parent::register();
}

Expand Down Expand Up @@ -133,15 +161,60 @@ public function process( ...$args ) {
*
* @return bool Whether transient caching of stylesheets is disabled.
*/
private function is_css_transient_caching_disabled() {
return AMP_Options_Manager::get_option( Option::DISABLE_CSS_TRANSIENT_CACHING, false );
public function is_css_transient_caching_disabled() {
return (bool) AMP_Options_Manager::get_option( Option::DISABLE_CSS_TRANSIENT_CACHING, false );
}

/**
* Enable transient caching of stylesheets.
*/
public function enable_css_transient_caching() {
AMP_Options_Manager::update_option( Option::DISABLE_CSS_TRANSIENT_CACHING, false );
}

/**
* Disable transient caching of stylesheets.
*/
private function disable_css_transient_caching() {
AMP_Options_Manager::update_option( Option::DISABLE_CSS_TRANSIENT_CACHING, true );
public function disable_css_transient_caching() {
AMP_Options_Manager::update_option(
Option::DISABLE_CSS_TRANSIENT_CACHING,
[
self::WP_VERSION => get_bloginfo( 'version' ),
self::GUTENBERG_VERSION => defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : null,
]
);
}

/**
* Sanitize the option.
*
* @param array $options Existing options.
* @param array $new_options New options.
* @return array Sanitized options.
*/
public function sanitize_disabled_option( $options, $new_options ) {
$value = null;

if ( array_key_exists( Option::DISABLE_CSS_TRANSIENT_CACHING, $new_options ) ) {
$unsanitized_value = $new_options[ Option::DISABLE_CSS_TRANSIENT_CACHING ];

if ( is_bool( $unsanitized_value ) ) {
$value = (bool) $unsanitized_value;
} elseif ( is_array( $unsanitized_value ) ) {
$value = [];
foreach ( wp_array_slice_assoc( $unsanitized_value, [ self::WP_VERSION, self::GUTENBERG_VERSION ] ) as $key => $version ) {
$value[ $key ] = preg_replace( '/[^a-z0-9_\-.]/', '', $version );
}
}
}

if ( empty( $value ) ) {
unset( $options[ Option::DISABLE_CSS_TRANSIENT_CACHING ] );
} else {
$options[ Option::DISABLE_CSS_TRANSIENT_CACHING ] = $value;
}

return $options;
}

/**
Expand All @@ -164,9 +237,39 @@ public function query_css_transient_count() {
* @param string $old_version Old version.
*/
public function handle_plugin_update( $old_version ) {
// Reset the disabling of the CSS caching subsystem when updating from versions 1.5.0 or 1.5.1.
if ( version_compare( $old_version, '1.5.0', '>=' ) && version_compare( $old_version, '1.5.2', '<' ) ) {
AMP_Options_Manager::update_option( Option::DISABLE_CSS_TRANSIENT_CACHING, false );
// Note: We cannot use the is_css_transient_caching_disabled method because we need to get the underlying stored value.
$disabled = AMP_Options_Manager::get_option( Option::DISABLE_CSS_TRANSIENT_CACHING, false );
if ( empty( $disabled ) ) {
return;
}

// Obtain the version of WordPress and Gutenberg at which time the functionality was disabled, if available.
$wp_version = isset( $disabled[ self::WP_VERSION ] ) ? $disabled[ self::WP_VERSION ] : null;
$gutenberg_version = isset( $disabled[ self::GUTENBERG_VERSION ] ) ? $disabled[ self::GUTENBERG_VERSION ] : null;

if (
// Reset the disabling of the CSS caching subsystem when updating from versions 1.5.0 or 1.5.1.
(
version_compare( $old_version, '1.5.0', '>=' )
&&
version_compare( $old_version, '1.5.2', '<' )
)
||
// Reset when it was disabled prior to the versions of WP/Gutenberg being captured,
// or if the captured versions were affected at the time of disabling.
(
version_compare( strtok( $old_version, '-' ), '2.2.2', '<' )
&&
(
! is_array( $disabled )
||
$this->block_uniqid_transformer->is_affected_gutenberg_version( $gutenberg_version )
||
$this->block_uniqid_transformer->is_affected_wordpress_version( $wp_version )
)
)
) {
$this->enable_css_transient_caching();
}
}

Expand Down
Loading

0 comments on commit 997b351

Please sign in to comment.