PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / trunk
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder vtrunk
2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 All 78 releases
ablocks / includes / performance / script-gate.php

script-gate.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder trunk, at includes/performance/script-gate.php

133 lines 4.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace ABlocks\Performance;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * The shared mechanism for stopping a <script> tag from executing, and letting
10 * something on the client release it later.
11 *
12 * Two features need exactly this. `DelayJs` holds aBlocks' own scripts back
13 * until the first user interaction; the cookie-consent addon holds third-party
14 * tags back until consent for their category is granted. Both do the same three
15 * things — decide whether a tag is in scope, rewrite its type so the browser
16 * parses but does not run it, and release it later — so the rewriting lives
17 * here once rather than as two independent regexes over every script on the
18 * page.
19 *
20 * The rewrite is deliberately textual. `script_loader_tag` hands over a string,
21 * and the consent addon's second layer works on already-rendered markup, so a
22 * DOM-based rewrite is not available in either caller.
23 */
24 class ScriptGate {
25
26 /**
27 * Marker type written into the tag, e.g. `ablocks/delayed` or `text/plain`.
28 *
29 * @var string
30 */
31 private $type;
32
33 /**
34 * `( $handle, $src, $tag ) => bool` — whether this tag is in scope.
35 *
36 * @var callable
37 */
38 private $in_scope;
39
40 /**
41 * `( $handle, $src ) => array` — extra attributes to write onto the tag.
42 *
43 * @var callable|null
44 */
45 private $attributes;
46
47 /**
48 * Whether to move `src` out of the way as well as changing the type.
49 *
50 * @var bool
51 */
52 private $move_src;
53
54 public function __construct( $type, callable $in_scope, $attributes = null, $move_src = false ) {
55 $this->type = $type;
56 $this->in_scope = $in_scope;
57 $this->attributes = $attributes;
58 $this->move_src = (bool) $move_src;
59 }
60
61 /**
62 * Start filtering enqueued script tags.
63 *
64 * @param int $priority Filter priority.
65 */
66 public function hook( $priority = 20 ) {
67 add_filter( 'script_loader_tag', [ $this, 'filter_tag' ], $priority, 3 );
68 }
69
70 /**
71 * @param string $tag Rendered script tag.
72 * @param string $handle Script handle.
73 * @param string $src Script source.
74 * @return string
75 */
76 public function filter_tag( $tag, $handle, $src ) {
77 if ( ! call_user_func( $this->in_scope, $handle, $src, $tag ) ) {
78 return $tag;
79 }
80 $attrs = $this->attributes ? (array) call_user_func( $this->attributes, $handle, $src ) : [];
81 return self::rewrite_tag( $tag, $this->type, $attrs, $this->move_src );
82 }
83
84 /**
85 * Rewrite one script tag so the browser will not execute it.
86 *
87 * A `<script>` whose type is not a JavaScript MIME type is parsed as a data
88 * block: it does not run, and — this is the part that matters for consent —
89 * a `src` on it is never fetched either, so no request reaches the third
90 * party before the visitor has decided.
91 *
92 * @param string $tag The full `<script …>` opening tag, or a whole tag pair.
93 * @param string $type Marker type to write.
94 * @param array $attrs Extra attributes, name => value.
95 * @param bool $move_src Whether to rename `src` to `data-ablocks-src`.
96 * @return string
97 */
98 public static function rewrite_tag( $tag, $type, array $attrs = [], $move_src = false ) {
99 if ( $move_src ) {
100 $tag = preg_replace( '/\ssrc=/', ' data-ablocks-src=', $tag, 1 );
101 }
102
103 $extra = '';
104 foreach ( $attrs as $name => $value ) {
105 $extra .= sprintf( '%s="%s" ', esc_attr( $name ), esc_attr( $value ) );
106 }
107
108 // An existing type is preserved so the release step can restore it —
109 // `type="module"` is not interchangeable with a classic script.
110 if ( preg_match( '/\stype=["\']([^"\']*)["\']/i', $tag, $found ) ) {
111 $extra .= sprintf( 'data-ablocks-type="%s" ', esc_attr( $found[1] ) );
112 $tag = preg_replace( '/\stype=["\'][^"\']*["\']/i', '', $tag, 1 );
113 }
114
115 return preg_replace(
116 '/^<script\s/',
117 sprintf( '<script type="%s" %s', esc_attr( $type ), $extra ),
118 $tag,
119 1
120 );
121 }
122
123 /**
124 * Whether a tag has already been gated by either feature.
125 *
126 * @param string $tag Script tag.
127 * @return bool
128 */
129 public static function is_gated( $tag ) {
130 return (bool) preg_match( '/\stype=["\'](?:text\/plain|ablocks\/[a-z-]+)["\']/i', $tag );
131 }
132 }
133