PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.12.0
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.12.0
2.13.0 2.13.1 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 All 80 releases
ablocks / addons / cookie-consent / gating.php

gating.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder 2.12.0, at addons/cookie-consent/gating.php

181 lines 4.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace ABlocksCookieConsent;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 use ABlocks\Performance\ScriptGate;
9
10 /**
11 * Layer one: enqueued scripts.
12 *
13 * Anything that went through `wp_enqueue_script` passes through
14 * `script_loader_tag`, which is the cheapest and safest place to gate — the
15 * handle and the source are both known, and nothing has been rendered yet.
16 *
17 * It is also, on a typical site, the minority of what needs gating. GA4, GTM
18 * and the Meta Pixel are usually pasted into a header hook rather than
19 * enqueued; those are `Buffer`'s problem.
20 *
21 * This class also owns the rule matching, because both layers ask the same
22 * question of different inputs.
23 */
24 class Gating {
25
26 /**
27 * Category matched by the most recent `in_scope` call, so the attribute
28 * callback does not have to run the rules a second time.
29 *
30 * @var string
31 */
32 private $matched = '';
33
34 public static function init() {
35 if ( ! Helper::is_gating_active() ) {
36 return;
37 }
38
39 $self = new self();
40
41 // Dry run walks the same rules and records what it would have done,
42 // without touching a single tag.
43 if ( Helper::get( 'dry_run', false ) ) {
44 add_filter( 'script_loader_tag', [ $self, 'observe_tag' ], 20, 3 );
45 return;
46 }
47
48 ( new ScriptGate(
49 'text/plain',
50 [ $self, 'in_scope' ],
51 [ $self, 'gate_attributes' ]
52 ) )->hook( 20 );
53 }
54
55 /**
56 * @param string $handle Script handle.
57 * @param string $src Script source.
58 * @param string $tag Rendered tag.
59 * @return bool
60 */
61 public function in_scope( $handle, $src, $tag = '' ) {
62 if ( $tag && ScriptGate::is_gated( $tag ) ) {
63 return false;
64 }
65 $this->matched = self::match_src( $src, $handle );
66 return '' !== $this->matched;
67 }
68
69 /**
70 * @return array
71 */
72 public function gate_attributes() {
73 return [ 'data-ablocks-consent' => $this->matched ];
74 }
75
76 /**
77 * Dry-run counterpart: record the verdict, return the tag untouched.
78 *
79 * @param string $tag Rendered tag.
80 * @param string $handle Script handle.
81 * @param string $src Script source.
82 * @return string
83 */
84 public function observe_tag( $tag, $handle, $src ) {
85 $category = self::match_src( $src, $handle );
86 if ( $category ) {
87 Report::add( $src ? $src : $handle, $category, 'enqueued' );
88 } elseif ( self::is_third_party( $src ) ) {
89 Report::add( $src, '', 'enqueued' );
90 }
91 return $tag;
92 }
93
94 /**
95 * Which category, if any, a script source or handle belongs to.
96 *
97 * @param string $src Script URL.
98 * @param string $handle Script handle.
99 * @return string Category slug, or '' when nothing matched.
100 */
101 public static function match_src( $src, $handle = '' ) {
102 $src = (string) $src;
103 if ( '' === $src && '' === (string) $handle ) {
104 return '';
105 }
106
107 foreach ( Helper::active_rules() as $rule ) {
108 if ( ! empty( $rule['handles'] ) ) {
109 $handles = array_map( 'trim', explode( ',', (string) $rule['handles'] ) );
110 if ( in_array( (string) $handle, $handles, true ) ) {
111 return $rule['category'];
112 }
113 }
114 if ( ! empty( $rule['src'] ) && '' !== $src && self::matches( $rule['src'], $src ) ) {
115 return $rule['category'];
116 }
117 }
118
119 return '';
120 }
121
122 /**
123 * Which category, if any, an inline script's contents belong to.
124 *
125 * @param string $code Inline script contents.
126 * @return string Category slug, or ''.
127 */
128 public static function match_inline( $code ) {
129 $code = (string) $code;
130 if ( '' === trim( $code ) ) {
131 return '';
132 }
133
134 foreach ( Helper::active_rules() as $rule ) {
135 if ( ! empty( $rule['inline'] ) && self::matches( $rule['inline'], $code ) ) {
136 return $rule['category'];
137 }
138 }
139
140 return '';
141 }
142
143 /**
144 * Run one rule pattern against a subject.
145 *
146 * Patterns come from the settings screen, so a malformed one is a user
147 * typo rather than a bug — it must not raise a warning on every page view,
148 * and it must never match by accident.
149 *
150 * @param string $pattern Regular-expression body.
151 * @param string $subject Text to test.
152 * @return bool
153 */
154 private static function matches( $pattern, $subject ) {
155 $result = @preg_match( '#' . str_replace( '#', '\#', $pattern ) . '#i', $subject ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a bad pattern is user input, not an error worth surfacing on every request.
156 return 1 === $result;
157 }
158
159 /**
160 * Whether a URL points somewhere other than this site.
161 *
162 * Used only for the "seen but not classified" report — a third-party script
163 * nobody wrote a rule for is exactly what a site owner needs told about.
164 *
165 * @param string $src Script URL.
166 * @return bool
167 */
168 public static function is_third_party( $src ) {
169 $src = (string) $src;
170 if ( '' === $src || 0 === strpos( $src, '/' ) || 0 === strpos( $src, 'data:' ) ) {
171 return false;
172 }
173 $host = wp_parse_url( $src, PHP_URL_HOST );
174 if ( ! $host ) {
175 return false;
176 }
177 $home = wp_parse_url( home_url(), PHP_URL_HOST );
178 return strtolower( $host ) !== strtolower( (string) $home );
179 }
180 }
181