Skip to content

Commit

Permalink
Merge pull request #7290 from ampproject/add/GA4-scripts-sanitizer
Browse files Browse the repository at this point in the history
Automatically mark GA4 scripts as being PX-verified
  • Loading branch information
westonruter authored Oct 27, 2022
2 parents b45ea20 + aeacc33 commit 86ad657
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 35 deletions.
4 changes: 4 additions & 0 deletions includes/amp-helper-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -1546,6 +1546,9 @@ function amp_get_content_sanitizers( $post = null ) {
// from getting removed in PWA plugin offline/500 templates.
AMP_PWA_Script_Sanitizer::class => [],

// The AMP_GTag_Script_Sanitizer runs before AMP_Script_Sanitizer to mark the the Google Analytics script tags as being PX-verified.
AMP_GTag_Script_Sanitizer::class => [],

// The AMP_Script_Sanitizer runs here because based on whether it allows custom scripts
// to be kept, it may impact the behavior of other sanitizers. For example, if custom
// scripts are kept then this is a signal that tree shaking in AMP_Style_Sanitizer cannot be
Expand Down Expand Up @@ -1716,6 +1719,7 @@ function amp_get_content_sanitizers( $post = null ) {
AMP_Core_Theme_Sanitizer::class, // Must come before script sanitizer since onclick attributes are removed.
AMP_Bento_Sanitizer::class, // Bento scripts may be preserved here.
AMP_PWA_Script_Sanitizer::class, // Must come before script sanitizer since PWA offline page scripts are removed.
AMP_GTag_Script_Sanitizer::class, // Must come before script sanitizer since gtag.js is removed.
AMP_Script_Sanitizer::class, // Must come before sanitizers for images, videos, audios, comments, forms, and styles.
AMP_Form_Sanitizer::class, // Must come before comments sanitizer.
AMP_Comments_Sanitizer::class, // Also must come after the form sanitizer.
Expand Down
87 changes: 87 additions & 0 deletions includes/sanitizers/class-amp-gtag-script-sanitizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php
/**
* GA4 Script Sanitizer.
*
* - This sanitizer will facilitate using GA4 while waiting on an AMP implementation.
* - This sanitizer will be only used in Moderate or Loose sandboxing level
*
* @since 2.3.1
* @package AMP
*/

use AmpProject\AmpWP\Option;
use AmpProject\AmpWP\ValidationExemption;

/**
* Class AMP_GTag_Script_Sanitizer
*
* @since 2.3.1
* @internal
*/
class AMP_GTag_Script_Sanitizer extends AMP_Base_Sanitizer {

/**
* Sanitize the AMP response for GA4 scripts.
*
* @since 2.3.1
*/
public function sanitize() {
if ( ! AMP_Options_Manager::get_option( Option::SANDBOXING_ENABLED ) ) {
return;
}

$sandboxing_level = AMP_Options_Manager::get_option( Option::SANDBOXING_LEVEL );

if ( 1 !== $sandboxing_level && 2 !== $sandboxing_level ) {
return;
}

/**
* GTag Script looks like this:
*
* <script async src="https://www.googletagmanager.com/gtag/js?id=xxxxxx"></script>
* <script>
* window.dataLayer = window.dataLayer || [];
* function gtag(){dataLayer.push(arguments);}
* gtag('js', new Date());
*
* gtag('config', 'xxxxxx');
* </script>
*/
$scripts = $this->dom->xpath->query( '//script[ ( @async and starts-with( @src, "https://www.googletagmanager.com/gtag/js" ) ) or contains( text(), "function gtag(" ) ]' );
if ( ! $scripts instanceof DOMNodeList || 0 === $scripts->length ) {
return;
}

foreach ( $scripts as $script ) {
ValidationExemption::mark_node_as_px_verified( $script );
}

/**
* Mark inline gtag events as PX verified attributes.
*
* Such inline events can look like:
*
* onclick="gtag('event','click', { 'event_category':"click", 'event_label':"contactPage" })"
* onsubmit="gtag('event','submit', { 'event_category':"submit", 'event_label':"contactPage" })"
* onkeypress="gtag('event','keypress', { 'event_category':"keypress", 'event_label':"contactPage" })"
*/
$inline_events = $this->dom->xpath->query(
'
//@*[
starts-with(name(), "on")
and
name() != "on"
and
contains(., "gtag(")
]
'
);

if ( $inline_events instanceof DOMNodeList ) {
foreach ( $inline_events as $inline_event ) {
ValidationExemption::mark_node_as_px_verified( $inline_event );
}
}
}
}
15 changes: 15 additions & 0 deletions src/Option.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,21 @@ interface Option {
*/
const SUPPRESSED_PLUGINS_USERNAME = 'username';


/**
* Option key for enabling sandboxing.
*
* @var string
*/
const SANDBOXING_ENABLED = 'sandboxing_enabled';

/**
* Option key for sandboxing level.
*
* @var string
*/
const SANDBOXING_LEVEL = 'sandboxing_level';

/**
* Version of the AMP plugin for which the options were last saved.
*
Expand Down
26 changes: 15 additions & 11 deletions src/Sandboxing.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use AMP_Style_Sanitizer;
use AmpProject\AmpWP\Infrastructure\Registerable;
use AmpProject\AmpWP\Infrastructure\Service;
use AmpProject\AmpWP\Option;
use AmpProject\Dom\Document;
use AmpProject\Dom\Element;
use AmpProject\Html\Attribute;
Expand All @@ -32,14 +33,17 @@ final class Sandboxing implements Service, Registerable {
/**
* Option key for enabling sandboxing.
*
* @deprecated Use Options interface for option keys.
*
* @var string
*/
const OPTION_ENABLED = 'sandboxing_enabled';

/**
* Option key for sandboxing level.
*
* @todo Move this to the Options interface once no longer experimental.
* @deprecated Use Options interface for option keys.
*
* @var string
*/
const OPTION_LEVEL = 'sandboxing_level';
Expand All @@ -57,11 +61,11 @@ final class Sandboxing implements Service, Registerable {
* @var array
*/
const DEFAULT_OPTIONS_SCHEMA = [
self::OPTION_ENABLED => [
Option::SANDBOXING_ENABLED => [
'type' => 'bool',
'default' => false,
],
self::OPTION_LEVEL => [
Option::SANDBOXING_LEVEL => [
'type' => 'int',
'enum' => self::LEVELS,
'default' => 1,
Expand Down Expand Up @@ -113,15 +117,15 @@ public function filter_default_options( $defaults ) {
* @return array Sanitized options.
*/
public function sanitize_options( $options, $new_options ) {
if ( isset( $new_options[ self::OPTION_ENABLED ] ) ) {
$options[ self::OPTION_ENABLED ] = (bool) $new_options[ self::OPTION_ENABLED ];
if ( isset( $new_options[ Option::SANDBOXING_ENABLED ] ) ) {
$options[ Option::SANDBOXING_ENABLED ] = (bool) $new_options[ Option::SANDBOXING_ENABLED ];
}
if (
isset( $new_options[ self::OPTION_LEVEL ] )
isset( $new_options[ Option::SANDBOXING_LEVEL ] )
&&
in_array( $new_options[ self::OPTION_LEVEL ], self::LEVELS, true )
in_array( $new_options[ Option::SANDBOXING_LEVEL ], self::LEVELS, true )
) {
$options[ self::OPTION_LEVEL ] = $new_options[ self::OPTION_LEVEL ];
$options[ Option::SANDBOXING_LEVEL ] = $new_options[ Option::SANDBOXING_LEVEL ];
}
return $options;
}
Expand All @@ -137,11 +141,11 @@ public function add_hooks() {
return;
}

if ( ! AMP_Options_Manager::get_option( self::OPTION_ENABLED ) ) {
if ( ! AMP_Options_Manager::get_option( Option::SANDBOXING_ENABLED ) ) {
return;
}

$sandboxing_level = AMP_Options_Manager::get_option( self::OPTION_LEVEL );
$sandboxing_level = AMP_Options_Manager::get_option( Option::SANDBOXING_LEVEL );

// Opt-in to the new script sanitization logic in the script sanitizer.
add_filter(
Expand Down Expand Up @@ -241,7 +245,7 @@ private function remove_required_amp_markup_if_not_used( Document $dom, $effecti
* @param int $effective_sandboxing_level Effective sandboxing level.
*/
public function finalize_document( Document $dom, $effective_sandboxing_level ) {
$actual_sandboxing_level = AMP_Options_Manager::get_option( self::OPTION_LEVEL );
$actual_sandboxing_level = AMP_Options_Manager::get_option( Option::SANDBOXING_LEVEL );

$meta_generator = $dom->xpath->query( '/html/head/meta[ @name = "generator" and starts-with( @content, "AMP Plugin" ) ]/@content' )->item( 0 );
if ( $meta_generator instanceof DOMAttr ) {
Expand Down
44 changes: 22 additions & 22 deletions tests/php/src/SandboxingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,15 @@ public function test_filter_rest_options_schema() {

$filtered = $this->instance->filter_rest_options_schema( $existing );
$this->assertArrayHasKey( 'foo', $filtered );
$this->assertArrayHasKey( Sandboxing::OPTION_LEVEL, $filtered );
$this->assertArrayHasKey( Option::SANDBOXING_LEVEL, $filtered );
}

/** @covers ::filter_default_options() */
public function test_filter_default_options() {
$this->assertEquals(
[
Sandboxing::OPTION_LEVEL => 1,
Sandboxing::OPTION_ENABLED => false,
Option::SANDBOXING_LEVEL => 1,
Option::SANDBOXING_ENABLED => false,
],
$this->instance->filter_default_options( [] )
);
Expand All @@ -77,34 +77,34 @@ public function test_filter_default_options() {
/** @covers ::sanitize_options() */
public function test_sanitize_options() {
$this->assertEquals(
[ Sandboxing::OPTION_LEVEL => 2 ],
[ Option::SANDBOXING_LEVEL => 2 ],
$this->instance->sanitize_options(
[ Sandboxing::OPTION_LEVEL => 2 ],
[ Sandboxing::OPTION_LEVEL => 'bad' ]
[ Option::SANDBOXING_LEVEL => 2 ],
[ Option::SANDBOXING_LEVEL => 'bad' ]
)
);

$this->assertEquals(
[ Sandboxing::OPTION_LEVEL => 3 ],
[ Option::SANDBOXING_LEVEL => 3 ],
$this->instance->sanitize_options(
[ Sandboxing::OPTION_LEVEL => 2 ],
[ Sandboxing::OPTION_LEVEL => 3 ]
[ Option::SANDBOXING_LEVEL => 2 ],
[ Option::SANDBOXING_LEVEL => 3 ]
)
);

$this->assertEquals(
[ Sandboxing::OPTION_LEVEL => 1 ],
[ Option::SANDBOXING_LEVEL => 1 ],
$this->instance->sanitize_options(
[ Sandboxing::OPTION_LEVEL => 1 ],
[ Sandboxing::OPTION_LEVEL => 0 ]
[ Option::SANDBOXING_LEVEL => 1 ],
[ Option::SANDBOXING_LEVEL => 0 ]
)
);

$this->assertEquals(
[ Sandboxing::OPTION_LEVEL => 1 ],
[ Option::SANDBOXING_LEVEL => 1 ],
$this->instance->sanitize_options(
[ Sandboxing::OPTION_LEVEL => 1 ],
[ Sandboxing::OPTION_LEVEL => 4 ]
[ Option::SANDBOXING_LEVEL => 1 ],
[ Option::SANDBOXING_LEVEL => 4 ]
)
);
}
Expand All @@ -114,9 +114,9 @@ public function test_add_hooks_not_standard_mode() {
$this->register_settings_and_set_user();
AMP_Options_Manager::update_options(
[
Sandboxing::OPTION_ENABLED => true,
Option::SANDBOXING_ENABLED => true,
Option::THEME_SUPPORT => AMP_Theme_Support::TRANSITIONAL_MODE_SLUG,
Sandboxing::OPTION_LEVEL => 2,
Option::SANDBOXING_LEVEL => 2,
]
);
$this->instance->add_hooks();
Expand All @@ -128,7 +128,7 @@ public function test_add_hooks_without_enabled_level() {
$this->register_settings_and_set_user();
AMP_Options_Manager::update_options(
[
Sandboxing::OPTION_ENABLED => false,
Option::SANDBOXING_ENABLED => false,
Option::THEME_SUPPORT => AMP_Theme_Support::STANDARD_MODE_SLUG,
]
);
Expand Down Expand Up @@ -178,9 +178,9 @@ public function test_add_hooks( $level, $expected_sanitizer_args ) {
$this->register_settings_and_set_user();
AMP_Options_Manager::update_options(
[
Sandboxing::OPTION_ENABLED => true,
Option::SANDBOXING_ENABLED => true,
Option::THEME_SUPPORT => AMP_Theme_Support::STANDARD_MODE_SLUG,
Sandboxing::OPTION_LEVEL => $level,
Option::SANDBOXING_LEVEL => $level,
]
);
$this->instance->add_hooks();
Expand Down Expand Up @@ -250,8 +250,8 @@ public function test_finalize_document_and_get_effective_level( $min_level, $bod
$this->register_settings_and_set_user();
AMP_Options_Manager::update_options(
[
Sandboxing::OPTION_ENABLED => true,
Sandboxing::OPTION_LEVEL => $min_level,
Option::SANDBOXING_ENABLED => true,
Option::SANDBOXING_LEVEL => $min_level,
]
);

Expand Down
Loading

0 comments on commit 86ad657

Please sign in to comment.