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 +383 -48 1.1.9 → 1.2.6 View file →
@@ -12,10 +12,10 @@
12 12 static $categorized_cookies = array();
13 13
14 14 static function enqueue_scripts(){
15 15 global $wpdb;
16 -
17 - $view = get_option('cookieadmin_law', 'cookieadmin_gdpr');
16 +
17 + $view = get_option('cookieadmin_law', 'cookieadmin_gdpr');
18 18 $policy = cookieadmin_load_policy();
19 19 $table_name = esc_sql($wpdb->prefix . 'cookieadmin_cookies');
20 20 //cookieadmin_r_print($view);
21 21 //cookieadmin_r_print($policy);
@@ -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 = [];
@@ -47,8 +49,9 @@
47 49
48 50 // Used for setting cookie
49 51 $policy[$view]['base_path'] = $base_path;
50 52
53 + // NOTE: Check the polylang string registration if changing these
51 54 $policy[$view]['lang']['show_less'] = __('Show less', 'cookieadmin');
52 55 $policy[$view]['lang']['duration'] = __('Duration', 'cookieadmin');
53 56 $policy[$view]['lang']['session'] = __('Session', 'cookieadmin');
54 57 $policy[$view]['lang']['days'] = __('Days', 'cookieadmin');
@@ -63,86 +66,414 @@
63 66 }
64 67
65 68 $policy[$view]['categorized_cookies'] = self::$categorized_cookies = $cookie_data;
66 69
70 + $policy[$view] = apply_filters('cookieadmin_before_localize', $policy[$view]);
71 +
67 72 wp_localize_script('cookieadmin_js', 'cookieadmin_policy', $policy[$view]);
68 73
69 74 }
70 75 }
71 76
72 - /* static function cookieadmin_block_cookie_init_php(){
77 + static function cookieadmin_block_cookie_init_php(){
73 78
74 - //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 +
75 89 $http_cookies = array();
76 - $headers = headers_list();
90 + $set_cookie_headers = array();
77 91
78 - foreach($headers as $header) {
79 -
80 - if (stripos(trim($header), 'Set-Cookie:') === 0) {
92 + foreach(headers_list() as $header) {
93 + if(stripos(trim($header), 'Set-Cookie:') === 0){
81 94 $header = trim(substr($header, strlen('Set-Cookie:')));
82 - $name = trim(explode('=', $header)[0]);
83 - $http_cookies[$name]['string'] = trim($header);
84 - setcookie($name, '', time() - 999999, '/');
95 + $set_cookie_headers[] = $header;
85 96 }
86 97 }
87 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 +
88 135 $http_cookies['cookieadmin_consent'] = ["string" => "cookieadmin_consent=CookieAdmin Cookie Initialization"];
89 136
90 137 self::$http_cookies = $http_cookies;
91 - } */
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 + }
92 279
93 - static function check_if_cookies_allowed($tag, $handle, $src){
280 + static function block_scripts(){
94 281
95 - $cookieadmin_consent = isset($_COOKIE['cookieadmin_consent'])
96 - ? json_decode(wp_unslash($_COOKIE['cookieadmin_consent']), true)
97 - : [];
282 + if(wp_doing_ajax() || is_admin() || defined('REST_REQUEST') || defined('COOKIEADMIN_SCANNER') || cookieadmin_is_editor_mode()){
283 + return;
284 + }
285 +
286 + $settings = get_option('cookieadmin_settings');
98 287
99 - array_walk( $cookieadmin_consent, function( $value, $key ) use ( &$cookieadmin_consent ) {
100 - $sanitized_key = sanitize_key( $key );
101 - $cookieadmin_consent[ $sanitized_key ] = sanitize_text_field($value);
102 - } );
288 + if(empty($settings) || empty($settings['block_scripts'])){
289 + return;
290 + }
291 +
292 + $view = get_option('cookieadmin_law', 'cookieadmin_gdpr');
293 + $policy = cookieadmin_load_policy();
294 + if(empty($policy) || empty($view)){
295 + return;
296 + }
297 +
298 + ob_start([__CLASS__, 'update_tracking_scripts']);
299 + }
300 +
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 + }
309 +
310 + private static function block_tracking_scripts($html){
311 + global $cookieadmin_settings;
312 +
313 + if(!is_string($html) || stripos($html, '<script') === false){
314 + return $html;
315 + }
316 +
317 + if(empty(self::$categorized_cookies)){
318 + return $html;
319 + }
320 +
321 + $cookieadmin_consent = self::get_consent();
322 +
323 + $len = strlen($html);
324 + $cursor = 0;
325 + $output = '';
326 +
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 + }
334 +
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;
347 + }
348 + }
349 +
350 + // Find end of opening tag '>' while respecting quotes
351 + $open_tag_end = false;
352 + $in_quote = false;
353 + $quote_char = '';
354 +
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;
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 + }
369 + }
370 + }
371 +
372 + if ($open_tag_end === false) {
373 + // Unclosed opening tag; append remainder and exit
374 + $output .= substr($html, $script_pos);
375 + break;
376 + }
377 +
378 + $attrs = substr($html, $after_script, $open_tag_end - $after_script);
379 +
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 + }
387 +
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);
391 +
392 + $output .= self::process_script_tag($attrs, $content, $full_tag, $cookieadmin_consent, $cookieadmin_settings);
393 + $cursor = $close_tag_end;
394 + }
395 +
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 + }
103 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 +
104 437 foreach (self::$categorized_cookies as $item) {
105 438 $category = !empty($item->category) ? strtolower($item->category) : '';
106 439 $patterns = !empty($item->patterns) ? json_decode($item->patterns, true) : '';
107 -
108 - if (!empty($patterns) && !empty($category)) {
109 - foreach ($patterns as $pattern) {
110 - if (strpos($src, $pattern) !== false) {
111 -
112 - if ( $category !== 'necessary' &&
113 - (empty($cookieadmin_consent) ||
114 - (!empty($cookieadmin_consent[$category]) && $cookieadmin_consent[$category] == 'false') ||
115 - (!empty($cookieadmin_consent['reject']) && $cookieadmin_consent['reject'] == 'true')
116 - )
117 - ) {
118 -
119 - // User has NOT consented -> block the script
120 440
121 - // Option 1 - completely remove script:
122 - // return '';
441 + if(empty($patterns) || empty($category)){
442 + continue;
443 + }
123 444
124 - // Option 2 - transform to type="text/plain"
125 - $tag = str_replace(
126 - '<script ',
127 - '<script type="text/plain" data-cookieadmin-category="' . esc_attr($category) . '" ',
128 - $tag
129 - );
130 -
131 - return $tag;
132 - }
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>';
133 459 }
134 460 }
135 461 }
136 462 }
137 463
138 - return $tag;
464 + return $full_tag;
139 465 }
140 466
141 467 static function cookieadmin_show_banner(){
142 -
143 - $view = get_option('cookieadmin_law', 'cookieadmin_gdpr');
468 +
469 + $view = get_option('cookieadmin_law', 'cookieadmin_gdpr');
144 470 $policy = cookieadmin_load_policy();
471 +
472 + // Do not show banner if banner is off via Geo rule
473 + if(empty($view)){
474 + return;
475 + }
145 476
146 477 $raw_template = cookieadmin_load_consent_template($policy[$view], $view);
147 478
148 479 if(!is_array($raw_template) || empty($raw_template)){
@@ -165,6 +496,10 @@
165 496 $query = $wpdb->prepare("SHOW TABLES LIKE %s", $table_name);
166 497
167 498 return $wpdb->get_var($query) === $table_name;
168 499 }
500 +
501 + static function memberpress_allow_style_prefix($prefix_arr){
502 + $prefix_arr[] = 'cookieadmin';
503 + return $prefix_arr;
504 + }
169 505 }
170 -