PluginProbe
CookieAdmin – Cookie Consent Banner / 1.2.6
CookieAdmin – Cookie Consent Banner v1.2.6
1.2.6 1.2.5 1.2.4 1.2.3 1.2.2 1.2.1 1.2.0 1.1.9 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 All 26 releases
← All changes | includes/enduser.php +337 -69 1.2.2 → 1.2.6 View file →
@@ -20,8 +20,10 @@
20 20 //cookieadmin_r_print($view);
21 21 //cookieadmin_r_print($policy);
22 22
23 23 if(!empty($policy) && !empty($view) && !cookieadmin_is_editor_mode()){
24 +
25 + add_filter('mepr_design_style_handle_prefixes', '\CookieAdmin\Enduser::memberpress_allow_style_prefix');
24 26
25 27 wp_enqueue_style('cookieadmin-style', COOKIEADMIN_PLUGIN_URL . 'assets/css/consent.css', [], COOKIEADMIN_VERSION);
26 28
27 29 $js_deps = [];
@@ -71,28 +73,210 @@
71 73
72 74 }
73 75 }
74 76
75 - /* static function cookieadmin_block_cookie_init_php(){
77 + static function cookieadmin_block_cookie_init_php(){
76 78
77 - //New - To catch, remove and send cookies in WP enqueue
79 + if(headers_sent() || (is_admin() && !wp_doing_ajax()) || defined('COOKIEADMIN_SCANNER') || cookieadmin_is_editor_mode()){
80 + return;
81 + }
82 +
83 + $view = get_option('cookieadmin_law', 'cookieadmin_gdpr');
84 +
85 + if(empty($view)){
86 + return;
87 + }
88 +
78 89 $http_cookies = array();
79 - $headers = headers_list();
90 + $set_cookie_headers = array();
80 91
81 - foreach($headers as $header) {
82 -
83 - if (stripos(trim($header), 'Set-Cookie:') === 0) {
92 + foreach(headers_list() as $header) {
93 + if(stripos(trim($header), 'Set-Cookie:') === 0){
84 94 $header = trim(substr($header, strlen('Set-Cookie:')));
85 - $name = trim(explode('=', $header)[0]);
86 - $http_cookies[$name]['string'] = trim($header);
87 - setcookie($name, '', time() - 999999, '/');
95 + $set_cookie_headers[] = $header;
88 96 }
89 97 }
90 98
99 + if(empty($set_cookie_headers)){
100 + return;
101 + }
102 +
103 + $policy = cookieadmin_load_policy();
104 +
105 + if(empty($policy) || empty($policy[$view])){
106 + return;
107 + }
108 +
109 + $categories = self::get_cookie_categories();
110 + $consent = self::get_consent();
111 + $preload = self::get_preload_categories($policy, $view);
112 + $allowed_headers = array();
113 +
114 + foreach($set_cookie_headers as $set_cookie){
115 + $name = self::get_cookie_name_from_header($set_cookie);
116 +
117 + if(empty($name)){
118 + continue;
119 + }
120 +
121 + if(self::is_response_cookie_allowed($set_cookie, $categories, $consent, $preload)){
122 + $allowed_headers[] = $set_cookie;
123 + continue;
124 + }
125 +
126 + $http_cookies[$name]['string'] = trim($set_cookie);
127 + }
128 +
129 + header_remove('Set-Cookie');
130 +
131 + foreach($allowed_headers as $set_cookie){
132 + header('Set-Cookie: ' . $set_cookie, false);
133 + }
134 +
91 135 $http_cookies['cookieadmin_consent'] = ["string" => "cookieadmin_consent=CookieAdmin Cookie Initialization"];
92 136
93 137 self::$http_cookies = $http_cookies;
94 - } */
138 + }
139 +
140 + static function get_cookie_categories(){
141 + global $wpdb;
142 +
143 + $table_name = $wpdb->prefix . 'cookieadmin_cookies';
144 + if(!self::cookieadmin_table_exists($table_name)){
145 + return array();
146 + }
147 +
148 + $categories = array(
149 + 'exact' => array(),
150 + 'prefix' => array(),
151 + );
152 + $rows = $wpdb->get_results("SELECT cookie_name, raw_name, category FROM {$table_name}");
153 +
154 + foreach($rows as $row){
155 + if(!empty($row->cookie_name) && !empty($row->category)){
156 + $category = strtolower($row->category);
157 + $categories['exact'][$row->cookie_name] = $category;
158 +
159 + if(!empty($row->raw_name)){
160 + $categories['exact'][$row->raw_name] = $category;
161 +
162 + if($row->raw_name !== $row->cookie_name && strpos($row->raw_name, $row->cookie_name) === 0){
163 + $categories['prefix'][$row->cookie_name] = $category;
164 + }
165 + }
166 + }
167 + }
168 +
169 + uksort($categories['prefix'], function($a, $b){
170 + return strlen($b) - strlen($a);
171 + });
172 +
173 + return $categories;
174 + }
175 +
176 + static function get_consent(){
177 + if(empty($_COOKIE['cookieadmin_consent'])){
178 + return array();
179 + }
180 +
181 + $consent = json_decode(wp_unslash($_COOKIE['cookieadmin_consent']), true);
182 + if(!is_array($consent)){
183 + return array();
184 + }
185 +
186 + $sanitized = array();
187 + foreach($consent as $key => $value){
188 + $sanitized[sanitize_key($key)] = sanitize_text_field($value);
189 + }
190 +
191 + return $sanitized;
192 + }
193 +
194 + static function get_preload_categories($policy, $view){
195 + if(empty($policy) || empty($policy[$view]['preload']) || !is_array($policy[$view]['preload'])){
196 + return array();
197 + }
198 +
199 + $preload = array();
200 + foreach($policy[$view]['preload'] as $category){
201 + $preload[] = strtolower(sanitize_key($category));
202 + }
203 +
204 + return $preload;
205 + }
206 +
207 + static function get_cookie_name_from_header($set_cookie){
208 + $parts = explode('=', $set_cookie, 2);
209 + $name = trim($parts[0]);
210 +
211 + if(empty($name)){
212 + return '';
213 + }
214 +
215 + return $name;
216 + }
217 +
218 + static function is_response_cookie_allowed($set_cookie, $categories, $consent, $preload = array()){
219 + $name = self::get_cookie_name_from_header($set_cookie);
220 +
221 + if(empty($name)){
222 + return false;
223 + }
224 +
225 + // A deletion never adds a cookie and must not be prevented.
226 + if(preg_match('/(?:^|;)\s*max-age\s*=\s*(-?\d+)/i', $set_cookie, $age) && (int) $age[1] <= 0){
227 + return true;
228 + }
229 +
230 + if(preg_match('/(?:^|;)\s*expires\s*=\s*([^;]+)/i', $set_cookie, $expires)){
231 + $expires_at = strtotime($expires[1]);
232 + if($expires_at !== false && $expires_at < time()){
233 + return true;
234 + }
235 + }
236 +
237 + if($name === 'cookieadmin_consent'){
238 + return true;
239 + }
240 +
241 + $category = self::get_cookie_category($name, $categories);
242 +
243 + if(empty($category)){
244 + return !empty($consent['accept']) && $consent['accept'] === 'true';
245 + }
246 +
247 + if($category === 'necessary' || in_array($category, $preload, true)){
248 + return true;
249 + }
250 +
251 + if(!empty($consent['reject']) && $consent['reject'] === 'true'){
252 + return false;
253 + }
254 +
255 + if(!empty($consent['accept']) && $consent['accept'] === 'true'){
256 + return true;
257 + }
258 +
259 + return !empty($consent[$category]) && $consent[$category] === 'true';
260 + }
261 +
262 + static function get_cookie_category($name, $categories){
263 + if(!empty($categories['exact'][$name])){
264 + return $categories['exact'][$name];
265 + }
266 +
267 + if(empty($categories['prefix'])){
268 + return '';
269 + }
270 +
271 + foreach($categories['prefix'] as $prefix => $category){
272 + if(strpos($name, $prefix) === 0){
273 + return $category;
274 + }
275 + }
276 +
277 + return '';
278 + }
95 279
96 280 static function block_scripts(){
97 281
98 282 if(wp_doing_ajax() || is_admin() || defined('REST_REQUEST') || defined('COOKIEADMIN_SCANNER') || cookieadmin_is_editor_mode()){
@@ -114,10 +298,20 @@
114 298 ob_start([__CLASS__, 'update_tracking_scripts']);
115 299 }
116 300
117 301 static function update_tracking_scripts($html){
302 + // Never let a blocking failure blank the page, serve it unmodified instead
303 + try{
304 + return self::block_tracking_scripts($html);
305 + }catch(\Throwable $e){
306 + return $html;
307 + }
308 + }
118 309
119 - if(stripos($html, '<script') === false){
310 + private static function block_tracking_scripts($html){
311 + global $cookieadmin_settings;
312 +
313 + if(!is_string($html) || stripos($html, '<script') === false){
120 314 return $html;
121 315 }
122 316
123 317 if(empty(self::$categorized_cookies)){
@@ -123,82 +317,152 @@
123 317 if(empty(self::$categorized_cookies)){
124 318 return $html;
125 319 }
126 320
127 - $cookieadmin_consent = isset($_COOKIE['cookieadmin_consent'])
128 - ? json_decode(wp_unslash($_COOKIE['cookieadmin_consent']), true)
129 - : [];
321 + $cookieadmin_consent = self::get_consent();
130 322
131 - // Sanitizing cookies
132 - array_walk( $cookieadmin_consent, function( $value, $key ) use ( &$cookieadmin_consent ) {
133 - $sanitized_key = sanitize_key( $key );
134 - $cookieadmin_consent[ $sanitized_key ] = sanitize_text_field($value);
135 - } );
323 + $len = strlen($html);
324 + $cursor = 0;
325 + $output = '';
136 326
137 - $html = preg_replace_callback(
138 - '/<script\b([^>]*)>([\s\S]*?)<\/script>/i',
139 - function($match) use ($cookieadmin_consent){
140 - $attrs = $match[1];
141 - $content = $match[2];
142 - $full_tag = $match[0];
327 + while ($cursor < $len) {
328 + // Find next <script tag
329 + $script_pos = stripos($html, '<script', $cursor);
330 + if ($script_pos === false) {
331 + $output .= substr($html, $cursor);
332 + break;
333 + }
143 334
144 - if(preg_match('/\btype\s*=\s*["\']text\/plain["\']/i', $attrs)){
145 - return $full_tag;
335 + // Append everything prior to this <script tag
336 + $output .= substr($html, $cursor, $script_pos - $cursor);
337 +
338 + // Check word boundary after '<script' (must be followed by whitespace, '>', or '/')
339 + $after_script = $script_pos + 7; // strlen('<script')
340 + if ($after_script < $len) {
341 + $next_char = $html[$after_script];
342 + if ($next_char !== ' ' && $next_char !== "\t" && $next_char !== "\n" && $next_char !== "\r" && $next_char !== '>' && $next_char !== '/') {
343 + // e.g. <scripture> or <scripts>
344 + $output .= substr($html, $script_pos, 7);
345 + $cursor = $after_script;
346 + continue;
146 347 }
348 + }
147 349
148 - if(preg_match('/\b(id|src)\s*=\s*["\'][^"\']*cookieadmin[^"\']*["\']/i', $attrs)){
149 - return $full_tag;
150 - }
350 + // Find end of opening tag '>' while respecting quotes
351 + $open_tag_end = false;
352 + $in_quote = false;
353 + $quote_char = '';
151 354
152 - if(preg_match('/\btype\s*=\s*["\']([^"\']+)["\']/i', $attrs, $type_match)){
153 - $type = strtolower(trim($type_match[1]));
154 - if($type !== 'text/javascript' && $type !== 'module'){
155 - return $full_tag;
355 + for ($i = $after_script; $i < $len; $i++) {
356 + $char = $html[$i];
357 + if ($in_quote) {
358 + if ($char === $quote_char) {
359 + $in_quote = false;
156 360 }
361 + } else {
362 + if ($char === '"' || $char === "'") {
363 + $in_quote = true;
364 + $quote_char = $char;
365 + } elseif ($char === '>') {
366 + $open_tag_end = $i;
367 + break;
368 + }
157 369 }
370 + }
158 371
159 - $src = '';
160 - if(preg_match('/\bsrc\s*=\s*["\']([^"\']*)["\']/i', $attrs, $src_match)){
161 - $src = $src_match[1];
162 - }
372 + if ($open_tag_end === false) {
373 + // Unclosed opening tag; append remainder and exit
374 + $output .= substr($html, $script_pos);
375 + break;
376 + }
163 377
164 - $match_against = !empty($src) ? $src : trim($attrs . ' ' . $content);
378 + $attrs = substr($html, $after_script, $open_tag_end - $after_script);
165 379
166 - if(empty($match_against)){
167 - return $full_tag;
168 - }
380 + // Find closing </script>
381 + $close_tag_pos = stripos($html, '</script>', $open_tag_end + 1);
382 + if ($close_tag_pos === false) {
383 + // Unclosed script tag; append remainder and exit
384 + $output .= substr($html, $script_pos);
385 + break;
386 + }
169 387
170 - foreach (self::$categorized_cookies as $item) {
171 - $category = !empty($item->category) ? strtolower($item->category) : '';
172 - $patterns = !empty($item->patterns) ? json_decode($item->patterns, true) : '';
388 + $content = substr($html, $open_tag_end + 1, $close_tag_pos - ($open_tag_end + 1));
389 + $close_tag_end = $close_tag_pos + 9; // strlen('</script>')
390 + $full_tag = substr($html, $script_pos, $close_tag_end - $script_pos);
173 391
174 - if(empty($patterns) || empty($category)){
175 - continue;
176 - }
392 + $output .= self::process_script_tag($attrs, $content, $full_tag, $cookieadmin_consent, $cookieadmin_settings);
393 + $cursor = $close_tag_end;
394 + }
177 395
178 - foreach ($patterns as $pattern) {
179 - if(strpos($match_against, $pattern) !== false){
180 - if($category !== 'necessary' &&
181 - (empty($cookieadmin_consent) ||
182 - (!empty($cookieadmin_consent[$category]) && $cookieadmin_consent[$category] == 'false') ||
183 - (!empty($cookieadmin_consent['reject']) && $cookieadmin_consent['reject'] == 'true')
184 - )
185 - ){
186 - if($attrs === ''){
187 - return '<script type="text/plain" data-cookieadmin-category="' . esc_attr($category) . '">' . $content . '</script>';
188 - }
189 - return '<script type="text/plain" data-cookieadmin-category="' . esc_attr($category) . '"' . $attrs . '>' . $content . '</script>';
190 - }
191 - }
396 + return $output;
397 + }
398 +
399 + private static function process_script_tag($attrs, $content, $full_tag, $cookieadmin_consent, $cookieadmin_settings){
400 + if(preg_match('/(?<![\w-])type\s*=\s*["\']text\/plain["\']/i', $attrs)){
401 + return $full_tag;
402 + }
403 +
404 + if(preg_match('/(?<![\w-])(id|src)\s*=\s*["\'][^"\']*cookieadmin[^"\']*["\']/i', $attrs)){
405 + return $full_tag;
406 + }
407 +
408 + if(preg_match('/(?<![\w-])type\s*=\s*["\']([^"\']+)["\']/i', $attrs, $type_match)){
409 + $type = strtolower(trim($type_match[1]));
410 + if($type !== 'text/javascript' && $type !== 'module'){
411 + return $full_tag;
412 + }
413 + }
414 +
415 + $src = '';
416 + if(preg_match('/(?<![\w-])src\s*=\s*["\']([^"\']*)["\']/i', $attrs, $src_match)){
417 + $src = $src_match[1];
418 + }
419 +
420 + $match_against = !empty($src) ? $src : trim($attrs . ' ' . $content);
421 +
422 + if(empty($match_against)){
423 + return $full_tag;
424 + }
425 +
426 + if(!empty($cookieadmin_settings['cookieadmin_google_advance_consent_mode'])){
427 + // External Google tag loader
428 + if(!empty($src) && stripos($src, 'googletagmanager.com/gtag/js') !== false){
429 + return $full_tag;
430 + }
431 + // Inline gtag() snippet
432 + if(empty($src) && stripos($content, 'gtag(') !== false){
433 + return $full_tag;
434 + }
435 + }
436 +
437 + foreach (self::$categorized_cookies as $item) {
438 + $category = !empty($item->category) ? strtolower($item->category) : '';
439 + $patterns = !empty($item->patterns) ? json_decode($item->patterns, true) : '';
440 +
441 + if(empty($patterns) || empty($category)){
442 + continue;
443 + }
444 +
445 + foreach ($patterns as $pattern) {
446 + if(strpos($match_against, $pattern) !== false){
447 + if($category !== 'necessary' &&
448 + (empty($cookieadmin_consent) ||
449 + (!empty($cookieadmin_consent[$category]) && $cookieadmin_consent[$category] == 'false') ||
450 + (!empty($cookieadmin_consent['reject']) && $cookieadmin_consent['reject'] == 'true')
451 + )
452 + ){
453 + // Remove any existing type attribute to prevent duplicate type="..." attributes
454 + $sanitized_attrs = preg_replace('/(?<![\w-])type\s*=\s*["\'][^"\']*["\']/i', '', $attrs);
455 + $sanitized_attrs = trim($sanitized_attrs);
456 + $attr_str = !empty($sanitized_attrs) ? ' ' . $sanitized_attrs : '';
457 +
458 + return '<script type="text/plain" data-cookieadmin-category="' . esc_attr($category) . '"' . $attr_str . '>' . $content . '</script>';
192 459 }
193 460 }
461 + }
462 + }
194 463
195 - return $full_tag;
196 - },
197 - $html
198 - );
199 -
200 - return $html;
464 + return $full_tag;
201 465 }
202 466
203 467 static function cookieadmin_show_banner(){
204 468
@@ -232,6 +496,10 @@
232 496 $query = $wpdb->prepare("SHOW TABLES LIKE %s", $table_name);
233 497
234 498 return $wpdb->get_var($query) === $table_name;
235 499 }
500 +
501 + static function memberpress_allow_style_prefix($prefix_arr){
502 + $prefix_arr[] = 'cookieadmin';
503 + return $prefix_arr;
504 + }
236 505 }
237 -