PluginProbe
SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking / trunk
SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking vtrunk
1.5.0 1.4.0 1.3.0 1.3.1 trunk 0.0.0-alpha.1 0.0.0-alpha.2 0.0.0-alpha.3 0.0.1-beta.1 0.0.1-beta.2 0.0.1-beta.3 0.0.1-beta.4 1.0.0 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4
surecookie / inc / modules / script-blocking / custom-scripts.php

custom-scripts.php in SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking trunk, at inc/modules/script-blocking/custom-scripts.php

171 lines 5.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Custom Blocked Scripts.
4 *
5 * Lets an admin manually add a script/iframe URL pattern to block under a
6 * chosen cookie category (the mirror of the Pro whitelist, which always
7 * allows). Entries are merged into the known-scripts dataset through the
8 * `surecookie_known_scripts` filter, so the whole existing pipeline - blocker
9 * rewriting, placeholder rendering, per-category consent gating and
10 * restore-on-consent - applies to them with no extra blocker logic.
11 *
12 * @package SureCookie\Inc\Modules\ScriptBlocking
13 * @since 1.3.0
14 */
15
16 namespace SureCookie\Inc\Modules\ScriptBlocking;
17
18 use SureCookie\Inc\Functions\Sanitize;
19 use SureCookie\Inc\Functions\Settings;
20 use SureCookie\Inc\Traits\GetInstance;
21
22 if ( ! defined( 'ABSPATH' ) ) {
23 exit; // Exit if accessed directly.
24 }
25
26 /**
27 * Custom_Scripts
28 *
29 * @since 1.3.0
30 */
31 class Custom_Scripts {
32 use GetInstance;
33
34 /**
35 * Setting key storing the admin's custom blocked-script entries
36 * ({ name, value, category } rows).
37 *
38 * @since 1.3.0
39 */
40 private const SETTING_KEY = 'custom_blocked_scripts';
41
42 /**
43 * Constructor.
44 *
45 * @since 1.3.0
46 */
47 private function __construct() {
48 // Priority 25: after the bundled/remote dataset (10) and the
49 // scan-detected merge (20), so an admin's explicit entry wins when the
50 // same pattern already exists (the blocker's pattern map is keyed by
51 // pattern - last write wins).
52 add_filter( 'surecookie_known_scripts', [ $this, 'merge_custom_blocked_scripts' ], 25 );
53 }
54
55 /**
56 * Merge the admin's custom blocked scripts into the known-scripts dataset.
57 *
58 * Each entry becomes a service under its chosen category. A rule the admin
59 * typed as script-only or iframe-only, or one carrying keywords, is marked
60 * `tag_scoped` so the blocker takes its arrays literally instead of pooling
61 * them across both passes.
62 *
63 * @param mixed $scripts Known scripts grouped by category.
64 * @since 1.3.0
65 * @return mixed
66 */
67 public function merge_custom_blocked_scripts( $scripts = null ) {
68 if ( ! is_array( $scripts ) ) {
69 return $scripts;
70 }
71
72 foreach ( $this->get_entries() as $entry ) {
73 $category = $entry['category'];
74
75 if ( ! isset( $scripts[ $category ] ) || ! is_array( $scripts[ $category ] ) ) {
76 $scripts[ $category ] = [];
77 }
78
79 // Honor the rule's resource type: script-only, iframe-only, or both.
80 $service = [
81 'label' => $entry['name'] !== '' ? $entry['name'] : $entry['value'],
82 ];
83 if ( $entry['type'] !== 'iframe' ) {
84 // Dependent JS keywords (e.g. "fbq, fbq.push") become extra
85 // patterns; the blocker matches them against inline content.
86 $service['scripts'] = array_merge( [ $entry['value'] ], $entry['keywords'] );
87 }
88 if ( $entry['type'] !== 'script' ) {
89 $service['iframes'] = [ $entry['value'] ];
90 }
91 if ( $entry['type'] !== 'any' || $entry['keywords'] !== [] ) {
92 // Keep the blocker from pooling these patterns across kinds the
93 // way it pools a catalog host: the admin picked a resource type,
94 // or the rule carries keywords, which are inline-JS identifiers
95 // and would match an unrelated first-party URL as a substring.
96 $service['tag_scoped'] = true;
97 }
98 if ( $entry['location'] !== 'any' ) {
99 // Region hint (head|body|footer): the blocker only matches the
100 // rule's script patterns inside that page section.
101 $service['location'] = $entry['location'];
102 }
103 if ( $entry['path'] !== '' ) {
104 // Narrowing constraint: the resource must ALSO contain this
105 // path/pattern, so a rule can target one file on a host.
106 $service['path'] = $entry['path'];
107 }
108
109 $scripts[ $category ][ 'custom-' . md5( $entry['value'] ) ] = $service;
110 }
111
112 return $scripts;
113 }
114
115 /**
116 * Read + normalize the custom blocked-script entries.
117 *
118 * Rows are { name, value, category, type, location, keywords }; the value
119 * (URL/domain pattern) is required, the category falls back to
120 * `uncategorized` (blocked until consent by default) when missing or
121 * unknown-typed, the type limits the rule to scripts, iframes, or both
122 * (`any`, the default), the location optionally restricts matching to a
123 * page region, and keywords are dependent-JS names blocked alongside.
124 *
125 * @since 1.3.0
126 * @return array<int, array{name: string, value: string, category: string, type: string, location: string, keywords: array<int, string>, path: string}>
127 */
128 private function get_entries(): array {
129 $list = Settings::get( self::SETTING_KEY );
130 if ( ! is_array( $list ) ) {
131 return [];
132 }
133
134 $out = [];
135 foreach ( $list as $entry ) {
136 if ( ! is_array( $entry ) ) {
137 continue;
138 }
139
140 $value = strtolower( trim( Sanitize::scalar( $entry['value'] ?? '' ) ) );
141 if ( $value === '' ) {
142 continue;
143 }
144
145 $category = sanitize_key( Sanitize::scalar( $entry['category'] ?? '' ) );
146 $type = Sanitize::scalar( $entry['type'] ?? '' );
147 $location = Sanitize::scalar( $entry['location'] ?? '' );
148 $path = strtolower( trim( Sanitize::scalar( $entry['path'] ?? '' ) ) );
149
150 // Comma-separated dependent-JS keywords, normalized to a clean list.
151 $keywords = array_values(
152 array_filter(
153 array_map( 'trim', explode( ',', Sanitize::scalar( $entry['keywords'] ?? '' ) ) )
154 )
155 );
156
157 $out[] = [
158 'name' => trim( Sanitize::scalar( $entry['name'] ?? '' ) ),
159 'value' => $value,
160 'category' => $category !== '' ? $category : 'uncategorized',
161 'type' => in_array( $type, [ 'script', 'iframe' ], true ) ? $type : 'any',
162 'location' => in_array( $location, [ 'head', 'body', 'footer' ], true ) ? $location : 'any',
163 'keywords' => $keywords,
164 'path' => $path,
165 ];
166 }
167
168 return $out;
169 }
170 }
171