PluginProbe
Cookie Compliance for WordPress – Cookie Consent, GDPR & CCPA / 3.1.9
Cookie Compliance for WordPress – Cookie Consent, GDPR & CCPA v3.1.9
3.1.9 3.1.8 3.1.7 3.1.5 3.1.6 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 3.0.3 3.0.2 trunk 1.0.0 1.0.1 1.1.0 1.2.0 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 All 135 releases
cookie-notice / cookie-notice.php

cookie-notice.php in Cookie Compliance for WordPress – Cookie Consent, GDPR & CCPA 3.1.9, at cookie-notice.php

2,048 lines 70.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: Cookie Compliance for WordPress – Cookie Consent, GDPR & CCPA
4 Description: Cookie Compliance for WordPress (formerly "Compliance by Hu-manity.co" / "Cookie Notice") — the WordPress component of Cookie Compliance, the consent management platform by Hu-manity.co. Cookie consent banner, pre-consent script blocking, Google Consent Mode v2 and consent records for GDPR, CCPA and global data privacy laws.
5 Version: 3.1.9
6 Author: Hu-manity.co
7 Author URI: https://hu-manity.co/
8 Plugin URI: https://cookie-compliance.co/
9 License: MIT License
10 License URI: https://opensource.org/licenses/MIT
11 Text Domain: cookie-notice
12 Domain Path: /languages
13
14 Cookie Compliance for WordPress
15 Copyright (C) 2026, Hu-manity.co - info@hu-manity.co
16
17 NAMING NOTE (DEC-007, 2026-08-18): the product is "Cookie Compliance", by Hu-manity.co. The
18 "cookie-notice" slug, text domain, option keys (cookie_notice_*), CSS classes and cn_/cookie_notice_
19 hook prefixes are LOAD-BEARING and intentionally do NOT match the brand. Renaming the wordpress.org
20 slug creates a new listing and forfeits 900K+ installs and 3,000+ reviews; changing the text domain
21 orphans every translation. Do not "tidy" them for consistency.
22
23 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
24
25 The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
26
27 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 */
29
30 // exit if accessed directly
31 if ( ! defined( 'ABSPATH' ) )
32 exit;
33
34 /**
35 * Cookie Notice class.
36 *
37 * @class Cookie_Notice
38 * @version 2.5.14
39 */
40 class Cookie_Notice {
41
42 /**
43 * React admin asset identifiers.
44 *
45 * Single source of truth for the bundle name, script handle, and inline
46 * data global emitted by wp_localize_script. Referenced by:
47 * - Cookie_Notice_Settings::admin_enqueue_scripts() — enqueue + localize
48 * - Cookie_Notice_Settings::add_react_admin_optimizer_attrs() — script_loader_tag filter
49 * - includes/modules/<vendor>/<vendor>.php — optimizer-exclusion filters
50 *
51 * Anything that grep-finds "REACT_ADMIN_" is a call site for these.
52 */
53 const REACT_ADMIN_HANDLE = 'cookie-notice-react-admin';
54 const REACT_ADMIN_BUNDLE_BASENAME = 'cn-admin-react.js';
55 const REACT_ADMIN_INLINE_KEYWORD = 'cnReactData';
56
57 private $status_data = [
58 'status' => '',
59 'subscription' => 'basic',
60 'widget_version' => '',
61 'threshold_exceeded' => false,
62 'activation_datetime' => 0
63 ];
64 private $x_api_key = 'hudft60djisdusdjwek';
65 private $app_host_url = 'https://app.hu-manity.co';
66 private $app_login_url = 'https://app.hu-manity.co/#/login';
67 private $app_dashboard_url = 'https://app.hu-manity.co/#/';
68 private $account_api_url = 'https://account-api.hu-manity.co';
69 private $designer_api_url = 'https://designer-api.hu-manity.co';
70 private $transactional_api_url = 'https://transactional-api.hu-manity.co';
71 private $app_widget_url = '//cdn.hu-manity.co/hu-banner.min.js';
72 private $deactivaion_url = '';
73 private $network_admin = false;
74 private $plugin_network_active = false;
75 private static $_instance;
76 private $notices = [];
77 public $options = [];
78 public $network_options = [];
79 public $bot_detect;
80 public $dashboard;
81 public $frontend;
82 public $settings;
83 public $consent_logs;
84 public $privacy_consent;
85 public $privacy_consent_logs;
86 public $welcome;
87 public $welcome_api;
88 public $welcome_frontend;
89 public $db_version;
90
91 /**
92 * @var $defaults
93 */
94 public $defaults = [
95 'general' => [
96 'global_override' => false,
97 'global_cookie' => false,
98 'app_id' => '',
99 'app_key' => '',
100 'app_blocking' => true,
101 'conditional_active' => false,
102 'conditional_display' => 'hide',
103 'conditional_rules' => [],
104 'amp_support' => false,
105 'bot_detection' => true,
106 'caching_compatibility' => true,
107 'debug_mode' => false,
108 'wp_consent_api' => true,
109 'excluded_handles' => [],
110 'position' => 'bottom',
111 'message_text' => '',
112 'css_class' => '',
113 'accept_text' => '',
114 'refuse_text' => '',
115 'refuse_opt' => false,
116 'refuse_code' => '',
117 'refuse_code_head' => '',
118 'revoke_cookies' => false,
119 'revoke_cookies_opt' => 'automatic',
120 'revoke_message_text' => '',
121 'revoke_text' => '',
122 'redirection' => false,
123 'see_more' => false,
124 'link_target' => '_blank',
125 'link_position' => 'banner',
126 'time' => 'month',
127 'time_rejected' => 'month',
128 'hide_effect' => 'fade',
129 'on_scroll' => false,
130 'on_scroll_offset' => 100,
131 'on_click' => false,
132 'colors' => [
133 'text' => '#fff',
134 'button' => '#00a99d',
135 'bar' => '#32323a',
136 'bar_opacity' => 100
137 ],
138 'see_more_opt' => [
139 'text' => '',
140 'link_type' => 'page',
141 'id' => 0,
142 'link' => '',
143 'sync' => false
144 ],
145 'script_placement' => 'header',
146 'translate' => true,
147 'deactivation_delete' => false,
148 'review_notice' => true,
149 'review_notice_delay' => 0,
150 'update_version' => 8,
151 'update_notice' => true,
152 'update_notice_diss' => false,
153 'update_delay_date' => 0,
154 'update_threshold_date' => 0,
155 'csp_notice' => false,
156 'ui_mode' => 'legacy',
157 // applied_template removed — now computed on the fly in React via matchTemplate().
158 'displayType' => 'floating',
159 ],
160 'privacy_consent' => [
161 'wordpress_active' => true,
162 'wordpress_active_type' => 'all',
163 'contactform7_active' => false,
164 'contactform7_active_type' => 'all',
165 'mailchimp_active' => false,
166 'mailchimp_active_type' => 'all',
167 'wpforms_active' => false,
168 'wpforms_active_type' => 'all',
169 'woocommerce_active' => false,
170 'woocommerce_active_type' => 'all',
171 'formidableforms_active' => false,
172 'formidableforms_active_type' => 'all',
173 'easydigitaldownloads_active' => false,
174 'easydigitaldownloads_active_type' => 'all'
175 ],
176 'data' => [
177 'status' => '',
178 'subscription' => 'basic',
179 'widget_version' => '',
180 'threshold_exceeded' => false,
181 'activation_datetime' => 0
182 ],
183 'version' => '3.1.9'
184 ];
185
186 /**
187 * Authoritative field-ownership partition (#2264).
188 *
189 * Only these keys may be written to cookie_notice_options by plugin code paths
190 * (save_options in react-admin-ajax.php, validate_options in settings.php).
191 *
192 * API-owned fields (bannerColor, primaryColor) are NEVER written here —
193 * cookie_notice_app_design is their exclusive store, populated by the
194 * Designer API via get_app_config().
195 *
196 * position and displayType are dual-homed: for connected sites the Designer
197 * API owns them (stored in cookie_notice_app_design); for disconnected sites
198 * there is no API path, so these must be writable here and are read from
199 * cookie_notice_options by the settings layer and React admin on load.
200 *
201 * @var string[]
202 */
203 public static $plugin_owned_fields = [
204 'message_text',
205 'position',
206 'displayType',
207 'accept_text',
208 'refuse_text',
209 'revoke_text',
210 'revoke_message_text',
211 'css_class',
212 'refuse_opt',
213 'revoke_cookies',
214 'revoke_cookies_opt',
215 'on_scroll',
216 'on_scroll_offset',
217 'on_click',
218 'redirection',
219 'see_more',
220 'see_more_opt',
221 'link_target',
222 'link_position',
223 'time',
224 'time_rejected',
225 'hide_effect',
226 'script_placement',
227 'bot_detection',
228 'amp_support',
229 'caching_compatibility',
230 'debug_mode',
231 'wp_consent_api',
232 'conditional_active',
233 'conditional_display',
234 'conditional_rules',
235 'deactivation_delete',
236 'app_blocking',
237 'excluded_handles',
238 'refuse_code',
239 'refuse_code_head',
240 'app_id',
241 'app_key',
242 'ui_mode',
243 'global_override',
244 'global_cookie',
245 'colors',
246 'redirect_delay',
247 'review_notice',
248 'review_notice_delay',
249 'update_version',
250 'update_notice',
251 'update_notice_diss',
252 'update_delay_date',
253 'update_threshold_date',
254 'csp_notice',
255 'translate',
256 ];
257
258 /**
259 * Disable object cloning.
260 *
261 * @return void
262 */
263 public function __clone() {}
264
265 /**
266 * Disable unserializing of the class.
267 *
268 * @return void
269 */
270 public function __wakeup() {}
271
272 /**
273 * Main plugin instance.
274 *
275 * @return object
276 */
277 public static function instance() {
278 if ( self::$_instance === null ) {
279 self::$_instance = new self();
280
281 add_action( 'init', [ self::$_instance, 'load_textdomain' ] );
282
283 self::$_instance->includes();
284
285 self::$_instance->bot_detect = new Cookie_Notice_Bot_Detect();
286 self::$_instance->dashboard = new Cookie_Notice_Dashboard();
287 self::$_instance->frontend = new Cookie_Notice_Frontend();
288 self::$_instance->settings = new Cookie_Notice_Settings();
289 new Cookie_Notice_React_Admin_Ajax();
290 self::$_instance->consent_logs = new Cookie_Notice_Consent_Logs();
291 self::$_instance->privacy_consent = new Cookie_Notice_Privacy_Consent();
292 self::$_instance->privacy_consent_logs = new Cookie_Notice_Privacy_Consent_Logs();
293 self::$_instance->welcome = new Cookie_Notice_Welcome();
294 self::$_instance->welcome_api = new Cookie_Notice_Welcome_API();
295 self::$_instance->welcome_frontend = new Cookie_Notice_Welcome_Frontend();
296 }
297
298 return self::$_instance;
299 }
300
301 /**
302 * Class constructor.
303 *
304 * @return void
305 */
306 public function __construct() {
307 // Allow wp-config.php overrides for staging/prod switching.
308 // Usage: define( 'CN_ACCOUNT_API_URL', 'https://stage-api.hu-manity.co' );
309 if ( defined( 'CN_ACCOUNT_API_URL' ) ) $this->account_api_url = CN_ACCOUNT_API_URL;
310 if ( defined( 'CN_DESIGNER_API_URL' ) ) $this->designer_api_url = CN_DESIGNER_API_URL;
311 if ( defined( 'CN_TRANSACTIONAL_API_URL' ) ) $this->transactional_api_url = CN_TRANSACTIONAL_API_URL;
312 if ( defined( 'CN_X_API_KEY' ) ) $this->x_api_key = CN_X_API_KEY;
313 if ( defined( 'CN_APP_WIDGET_URL' ) ) $this->app_widget_url = CN_APP_WIDGET_URL;
314 if ( defined( 'CN_APP_HOST_URL' ) ) $this->app_host_url = CN_APP_HOST_URL;
315
316 // define plugin constants
317 $this->define_constants();
318
319 // activation hooks
320 register_activation_hook( __FILE__, [ $this, 'activation' ] );
321 register_deactivation_hook( __FILE__, [ $this, 'deactivation' ] );
322
323 // set network data
324 $this->set_network_data();
325
326 $this->check_legacy_options();
327
328 // get options
329 if ( is_multisite() ) {
330 // get network options
331 $this->network_options['general'] = get_site_option( 'cookie_notice_options', $this->defaults['general'] );
332 $this->network_options['privacy_consent'] = get_site_option( 'cookie_notice_privacy_consent', $this->defaults['privacy_consent'] );
333
334 if ( $this->is_network_admin() ) {
335 $general_options = $this->network_options['general'];
336 $privacy_consent_options = $this->network_options['privacy_consent'];
337 } else {
338 $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
339
340 // settings page?
341 if ( is_admin() && $page === 'cookie-notice' ) {
342 // get current url path
343 $url_path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );
344
345 if ( is_string( $url_path ) && basename( $url_path ) === 'admin.php' ) {
346 // get site options
347 $general_options = get_option( 'cookie_notice_options', $this->defaults['general'] );
348 $privacy_consent_options = get_option( 'cookie_notice_privacy_consent', $this->defaults['privacy_consent'] );
349 }
350 } else {
351 if ( $this->is_plugin_network_active() && $this->network_options['general']['global_override'] ) {
352 $general_options = $this->network_options['general'];
353 $privacy_consent_options = $this->network_options['privacy_consent'];
354 } else {
355 $general_options = get_option( 'cookie_notice_options', $this->defaults['general'] );
356 $privacy_consent_options = get_option( 'cookie_notice_privacy_consent', $this->defaults['privacy_consent'] );
357 }
358 }
359 }
360 } else {
361 $general_options = get_option( 'cookie_notice_options', $this->defaults['general'] );
362 $privacy_consent_options = get_option( 'cookie_notice_privacy_consent', $this->defaults['privacy_consent'] );
363 }
364
365 // merge old options with new ones
366 $this->options['general'] = $this->multi_array_merge( $this->defaults['general'], $general_options );
367 $this->options['privacy_consent'] = $this->multi_array_merge( $this->defaults['privacy_consent'], $privacy_consent_options );
368
369 if ( ! isset( $this->options['general']['see_more_opt']['sync'] ) )
370 $this->options['general']['see_more_opt']['sync'] = $this->defaults['general']['see_more_opt']['sync'];
371
372 // actions
373 add_action( 'plugins_loaded', [ $this, 'set_database_version' ], 0 );
374 add_action( 'plugins_loaded', [ $this, 'set_status_data' ], 0 );
375 add_action( 'init', [ $this, 'register_shortcodes' ] );
376 add_action( 'init', [ $this, 'wpsc_add_cookie' ] );
377 add_action( 'init', [ $this, 'maybe_apply_dev_tier_override' ] );
378 add_action( 'init', [ $this, 'set_plugin_links' ] );
379 add_action( 'admin_init', [ $this, 'update_notice' ] );
380 add_action( 'admin_init', [ $this, 'maybe_redirect_after_activation' ] );
381 add_action( 'admin_init', [ $this, 'maybe_show_license_assigned_notice' ] );
382 add_action( 'admin_init', [ $this, 'maybe_switch_ui_mode' ] );
383 add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
384 add_action( 'admin_footer', [ $this, 'deactivate_plugin_template' ] );
385 add_action( 'wp_ajax_cn_dismiss_notice', [ $this, 'ajax_dismiss_admin_notice' ] );
386 add_action( 'wp_ajax_cn_review_notice', [ $this, 'ajax_review_notice' ] );
387 add_action( 'wp_ajax_cn-deactivate-plugin', [ $this, 'deactivate_plugin' ] );
388 }
389
390 /**
391 * Set current plugin version from database.
392 *
393 * @return void
394 */
395 public function set_database_version() {
396 // get current version
397 if ( $this->is_network_admin() )
398 $this->db_version = get_site_option( 'cookie_notice_version', '1.0.0' );
399 else
400 $this->db_version = get_option( 'cookie_notice_version', '1.0.0' );
401 }
402
403 /**
404 * Check legacy options.
405 *
406 * @return void
407 */
408 public function check_legacy_options() {
409 // multisite?
410 if ( is_multisite() ) {
411 // get network options
412 $site_options = get_site_option( 'cookie_notice_options', $this->defaults['general'] );
413
414 // update legacy options
415 $site_options = $this->update_legacy_options( $site_options );
416
417 // any changes?
418 if ( $site_options !== false )
419 update_site_option( 'cookie_notice_options', $site_options );
420 }
421
422 // get options
423 $options = get_option( 'cookie_notice_options', $this->defaults['general'] );
424
425 // update legacy options
426 $options = $this->update_legacy_options( $options );
427
428 // any changes?
429 if ( $options !== false )
430 update_option( 'cookie_notice_options', $options );
431 }
432
433 /**
434 * Maybe change legacy options.
435 *
436 * @param array $options
437 * @return false|array
438 */
439 public function update_legacy_options( $options ) {
440 // bail out if options are missing or invalid to avoid PHP 8 fatal on non-array values
441 if ( ! is_array( $options ) )
442 return $this->defaults['general'];
443
444 $options_changed = false;
445
446 // check legacy parameters that were yes/no strings
447 foreach ( [ 'refuse_opt', 'on_scroll', 'on_click', 'deactivation_delete', 'see_more' ] as $param ) {
448 if ( array_key_exists( $param, $options ) && ! is_bool( $options[$param] ) ) {
449 $options[$param] = $options[$param] === 'yes';
450
451 $options_changed = true;
452 }
453 }
454
455 // migrate banner_size → displayType (#2269)
456 if ( array_key_exists( 'banner_size', $options ) ) {
457 $options['displayType'] = $options['banner_size'];
458 unset( $options['banner_size'] );
459
460 $options_changed = true;
461 } elseif ( ! array_key_exists( 'displayType', $options ) ) {
462 $options['displayType'] = 'floating';
463
464 $options_changed = true;
465 }
466
467 // check hide banner
468 if ( isset( $options['hide_banner'] ) ) {
469 if ( $options['hide_banner'] && ! isset( $options['conditional_active'] ) ) {
470 $options['conditional_active'] = true;
471 $options['conditional_display'] = 'hide';
472 $options['conditional_rules'] = [
473 1 => [
474 1 => [
475 'param' => 'user_type',
476 'operator' => 'equal',
477 'value' => 'logged_in'
478 ]
479 ]
480 ];
481 }
482
483 unset( $options['hide_banner'] );
484
485 $options_changed = true;
486 }
487
488 if ( $options_changed )
489 return $options;
490 else
491 return false;
492 }
493
494 /**
495 * Setup plugin constants.
496 *
497 * @return void
498 */
499 private function define_constants() {
500 define( 'COOKIE_NOTICE_URL', plugins_url( '', __FILE__ ) );
501 define( 'COOKIE_NOTICE_PATH', plugin_dir_path( __FILE__ ) );
502 define( 'COOKIE_NOTICE_BASENAME', plugin_basename( __FILE__ ) );
503 define( 'COOKIE_NOTICE_REL_PATH', dirname( COOKIE_NOTICE_BASENAME ) );
504 }
505
506 /**
507 * Set cookie compliance status data.
508 *
509 * @return void
510 */
511 public function set_status_data() {
512 $default_data = $this->defaults['data'];
513
514 if ( is_multisite() ) {
515 if ( $this->is_plugin_network_active() ) {
516 // network
517 if ( $this->is_network_admin() ) {
518 if ( $this->network_options['general']['global_override'] )
519 $status_data = get_site_option( 'cookie_notice_status', $default_data );
520 else
521 $status_data = $default_data;
522 // site
523 } else {
524 if ( $this->network_options['general']['global_override'] )
525 $status_data = get_site_option( 'cookie_notice_status', $default_data );
526 else
527 $status_data = get_option( 'cookie_notice_status', $default_data );
528 }
529 } else {
530 // network
531 if ( $this->is_network_admin() )
532 $status_data = $default_data;
533 // site
534 else
535 $status_data = get_option( 'cookie_notice_status', $default_data );
536 }
537 } else
538 $status_data = get_option( 'cookie_notice_status', $default_data );
539
540 // old status format?
541 if ( ! is_array( $status_data ) ) {
542 // update config data
543 $status_data = $this->welcome_api->get_app_config( '', true );
544 } else {
545 // merge database data with default data
546 $status_data = array_merge( $default_data, $status_data );
547 }
548
549 if ( $status_data['threshold_exceeded'] )
550 $this->options['general']['app_blocking'] = false;
551
552 // check status
553 $status = $this->check_status( $status_data['status'] );
554
555 // no activation timestamp?
556 if ( empty( $status_data['activation_datetime'] ) ) {
557 if ( $status === 'active' )
558 $activation = time();
559 else
560 $activation = 0;
561 } else
562 $activation = (int) $status_data['activation_datetime'];
563
564 // set status data
565 $this->status_data = [
566 'status' => $status,
567 'subscription' => $this->check_subscription( $status_data['subscription'] ),
568 'widget_version' => isset( $status_data['widget_version'] ) ? $status_data['widget_version'] : '',
569 'threshold_exceeded' => (bool) $status_data['threshold_exceeded'],
570 'activation_datetime' => $activation
571 ];
572
573 }
574
575 /**
576 * Get cookie compliance status data.
577 *
578 * @return string
579 */
580 public function get_status_data() {
581 return $this->status_data;
582 }
583
584 /**
585 * Get cookie compliance status.
586 *
587 * @return string
588 */
589 public function get_status() {
590 return $this->status_data['status'];
591 }
592
593 /**
594 * Check cookie compliance status.
595 *
596 * @param string $status
597 * @return string
598 */
599 public function check_status( $status ) {
600 $status = sanitize_key( $status );
601
602 return ! empty( $status ) && in_array( $status, [ 'active', 'pending' ], true ) ? $status : $this->defaults['data']['status'];
603 }
604
605 /**
606 * Get cookie compliance subscription.
607 *
608 * @return string
609 */
610 /**
611 * CN_DEV_MODE: Apply ?cn_tier override.
612 *
613 * Hooked on 'init' so current_user_can() is available (it is NOT at plugins_loaded:0).
614 * Overrides status_data + app_id in-memory for the current request.
615 */
616 public function maybe_apply_dev_tier_override() {
617 if ( ! defined( 'CN_DEV_MODE' ) || ! CN_DEV_MODE )
618 return;
619
620 if ( ! current_user_can( 'manage_options' ) )
621 return;
622
623 $cn_tier = isset( $_GET['cn_tier'] ) ? sanitize_key( $_GET['cn_tier'] ) : '';
624
625 // 'basic' still accepted as a legacy alias for the Banner Only state, so an
626 // old dev bookmark keeps working. Note the VALUE written below stays
627 // 'basic': that is the platform's wire label for the Free plan (minted from
628 // the DB's 'free' by Designer API userDesignLive.controller.ts:78), so it
629 // must match what a real response would store. The disconnected state is
630 // expressed by clearing app_id, never by the subscription value.
631 if ( $cn_tier === 'banner_only' || $cn_tier === 'basic' ) {
632 $this->status_data['subscription'] = 'basic';
633 $this->options['general']['app_id'] = '';
634 } elseif ( $cn_tier === 'free' ) {
635 $this->status_data['subscription'] = 'basic';
636 if ( empty( $this->options['general']['app_id'] ) )
637 $this->options['general']['app_id'] = 'cn-dev-free-plan';
638 } elseif ( $cn_tier === 'pro' ) {
639 $this->status_data['subscription'] = 'pro';
640 if ( empty( $this->options['general']['app_id'] ) )
641 $this->options['general']['app_id'] = 'cn-dev-pro-plan';
642 }
643 }
644
645 public function get_subscription() {
646 return $this->status_data['subscription'];
647 }
648
649 /**
650 * Resolve which Web Channel banner build to load.
651 *
652 * Backend-controlled: the server-fed WidgetVersion flag (Application table,
653 * delivered on the get_config response alongside SubscriptionType) is the
654 * SOLE source of the channel decision. There is no plugin UI and no
655 * tier-based fallback — cohorts are flipped in the database.
656 *
657 * - 'v2' -> v2 build.
658 * - anything else / '' -> v1 (the current serving script; safe default).
659 *
660 * v1 by default means a fresh install — which has no widget_version until
661 * its first successful config pull — never ships v2 before the backend
662 * explicitly opts it in.
663 *
664 * @return string 'v1' | 'v2'
665 */
666 public function get_banner_channel() {
667 return $this->status_data['widget_version'] === 'v2' ? 'v2' : 'v1';
668 }
669
670 /**
671 * Canonical optimizer/CDN skip attributes for plugin-owned <script> tags.
672 *
673 * Single source of truth for the data-cfasync / nowprocket / noptimize /
674 * nitro-exclude / jetpack-boost / no-minify attribute set that tells caching
675 * and optimizer plugins to leave our scripts alone. Consumed by the
676 * script_loader_tag filters in Cookie_Notice_Settings and
677 * Cookie_Notice_Dashboard; the inline frontend banner tags
678 * ( frontend.php / welcome-frontend.php ) carry the same literal string —
679 * if this set changes, update those heredocs too.
680 *
681 * @return string Leading-space attribute string, ready to append after '<script'.
682 */
683 public static function optimizer_skip_attrs() {
684 return ' data-cfasync="false" data-nowprocket data-noptimize="1" data-no-optimize="1" nitro-exclude data-jetpack-boost="ignore" data-no-minify';
685 }
686
687 /**
688 * Insert a '/v2/' path segment before the filename of a widget URL.
689 *
690 * Splits any query string off first so it stays trailing
691 * ( …/v2/hu-banner.min.js?ver=1, not …/hu-banner.min.js/v2/?ver=1 ), and
692 * only injects when the URL actually has a path segment after the host —
693 * a host-only override ( '//cdn.example.com', 'https://cdn.example.com' )
694 * has no filename to sit beside, so it is returned unchanged rather than
695 * emit a broken 'https:/v2//…' URL. The contract for CN_APP_WIDGET_URL is
696 * therefore "must point at a file"; anything else stays on the v1 shape.
697 *
698 * @param string $url Base widget URL (no query string assumptions).
699 * @return string
700 */
701 private function inject_v2_segment( $url ) {
702 // peel off ?query / #fragment so the segment lands before the filename
703 $suffix = '';
704 $cut = strcspn( $url, '?#' );
705
706 if ( $cut < strlen( $url ) ) {
707 $suffix = substr( $url, $cut );
708 $url = substr( $url, 0, $cut );
709 }
710
711 $slash = strrpos( $url, '/' );
712
713 // Need a real filename to sit the segment beside: bail if there is no
714 // slash, if the only slash(es) are the scheme's '//' (host-only, e.g.
715 // 'https://cdn.example.com'), or if nothing follows the final slash
716 // (trailing-slash host, e.g. 'https://cdn.example.com/'). Any of these
717 // stays on the v1 shape rather than emit a broken or file-less URL.
718 $filename = $slash === false ? '' : substr( $url, $slash + 1 );
719
720 if ( $slash === false || $slash === strpos( $url, '//' ) + 1 || $filename === '' )
721 return $url . $suffix;
722
723 return substr( $url, 0, $slash + 1 ) . 'v2/' . $filename . $suffix;
724 }
725
726 /**
727 * Check cookie compliance subscription.
728 *
729 * @param string $subscription
730 * @return string
731 */
732 public function check_subscription( $subscription ) {
733 $subscription = sanitize_key( $subscription );
734
735 return ! empty( $subscription ) && in_array( $subscription, [ 'basic', 'pro' ], true ) ? $subscription : $this->defaults['data']['subscription'];
736 }
737
738 /**
739 * Check whether the current threshold is exceeded.
740 *
741 * @return bool
742 */
743 public function threshold_exceeded() {
744 return $this->status_data['threshold_exceeded'];
745 }
746
747 /**
748 * Get cookie compliance activation timestamp.
749 *
750 * @return int
751 */
752 public function get_cc_activation_datetime() {
753 return (int) $this->status_data['activation_datetime'];
754 }
755
756 /**
757 * Get endpoint URL.
758 *
759 * @param string $type
760 * @param string $query
761 * @return string
762 */
763 public function get_url( $type, $query = '' ) {
764 if ( $type === 'login' )
765 $url = $this->app_login_url;
766 elseif ( $type === 'dashboard' )
767 $url = $this->app_dashboard_url;
768 elseif ( $type === 'widget' ) {
769 $url = $this->app_widget_url;
770
771 // Inject a '/v2/' path segment before the filename when the resolved
772 // channel is v2 (so //cdn.hu-manity.co/hu-banner.min.js becomes
773 // //cdn.hu-manity.co/v2/hu-banner.min.js, and any CN_APP_WIDGET_URL
774 // override carries to v2 too). v1 returns the base unchanged.
775 if ( $this->get_banner_channel() === 'v2' )
776 $url = $this->inject_v2_segment( $url );
777 }
778 elseif ( $type === 'react-admin' )
779 $url = COOKIE_NOTICE_URL . '/assets/react-admin/' . self::REACT_ADMIN_BUNDLE_BASENAME;
780 elseif ( $type === 'host' )
781 $url = $this->app_host_url;
782 elseif ( $type === 'account_api' )
783 $url = $this->account_api_url;
784 elseif ( $type === 'designer_api' )
785 $url = $this->designer_api_url;
786 elseif ( $type === 'transactional_api' )
787 $url = $this->transactional_api_url;
788
789 return $url . ( $query !== '' ? $query : '' );
790 }
791
792 /**
793 * Get API key.
794 *
795 * @return string
796 */
797 public function get_api_key() {
798 return $this->x_api_key;
799 }
800
801 /**
802 * Check whether the current request is for the network administrative interface.
803 *
804 * @return bool
805 */
806 public function is_network_admin() {
807 return $this->network_admin;
808 }
809
810 /**
811 * Check whether the plugin is active for the entire network.
812 *
813 * @return bool
814 */
815 public function is_plugin_network_active() {
816 return $this->plugin_network_active;
817 }
818
819 /**
820 * Check whether network-wide options should be used.
821 *
822 * Returns true when the plugin is network-active with global_override enabled,
823 * meaning all sites share the network-level configuration.
824 *
825 * @return bool
826 */
827 public function is_network_options() {
828 return is_multisite() && $this->is_plugin_network_active() && $this->network_options['general']['global_override'];
829 }
830
831 /**
832 * Set network data.
833 *
834 * @return void
835 */
836 private function set_network_data() {
837 // load plugin.php file
838 if ( ! function_exists( 'is_plugin_active_for_network' ) )
839 require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
840
841 $cn_network = isset( $_POST['cn_network'] ) ? (int) $_POST['cn_network'] : false;
842
843 // bypass is_network_admin() to handle AJAX requests properly.
844 $this->network_admin = is_multisite() && ( is_network_admin() || ( wp_doing_ajax() && $cn_network === 1 ) );
845
846 // check whether the plugin is active for the entire network.
847 $this->plugin_network_active = is_plugin_active_for_network( COOKIE_NOTICE_BASENAME );
848 }
849
850 /**
851 * Include required files.
852 *
853 * @return void
854 */
855 private function includes() {
856 include_once( COOKIE_NOTICE_PATH . 'includes/bot-detect.php' );
857 include_once( COOKIE_NOTICE_PATH . 'includes/dashboard.php' );
858 include_once( COOKIE_NOTICE_PATH . 'includes/frontend.php' );
859 include_once( COOKIE_NOTICE_PATH . 'includes/functions.php' );
860 include_once( COOKIE_NOTICE_PATH . 'includes/settings.php' );
861 include_once( COOKIE_NOTICE_PATH . 'includes/react-admin-ajax.php' );
862 include_once( COOKIE_NOTICE_PATH . 'includes/consent-logs.php' );
863 include_once( COOKIE_NOTICE_PATH . 'includes/privacy-consent.php' );
864 include_once( COOKIE_NOTICE_PATH . 'includes/privacy-consent-logs.php' );
865 include_once( COOKIE_NOTICE_PATH . 'includes/welcome.php' );
866 include_once( COOKIE_NOTICE_PATH . 'includes/welcome-api.php' );
867 include_once( COOKIE_NOTICE_PATH . 'includes/welcome-frontend.php' );
868 include_once( COOKIE_NOTICE_PATH . 'includes/modules/wp-consent-api/wp-consent-api.php' );
869 }
870
871 /**
872 * Load textdomain.
873 *
874 * @return void
875 */
876 public function load_textdomain() {
877 load_plugin_textdomain( 'cookie-notice', false, dirname( COOKIE_NOTICE_BASENAME ) . '/languages/' );
878 }
879
880 /**
881 * Plugin activation.
882 *
883 * @global object $wpdb
884 *
885 * @param bool $network
886 * @return void
887 */
888 public function activation( $network ) {
889 // New installs start with React UI. We keep the compile-time default as
890 // 'legacy' so existing sites (which may lack ui_mode in the DB) continue
891 // to see the legacy interface through the multi_array_merge() fallback.
892 $activation_defaults = $this->defaults['general'];
893 $activation_defaults['ui_mode'] = 'react';
894
895 // network activation?
896 if ( is_multisite() && $network ) {
897 // add network options
898 add_site_option( 'cookie_notice_options', $activation_defaults );
899 add_site_option( 'cookie_notice_privacy_consent', $this->defaults['privacy_consent'] );
900 add_site_option( 'cookie_notice_status', $this->defaults['data'] );
901 add_site_option( 'cookie_notice_version', $this->defaults['version'] );
902
903 // Reactivations: switch network-level option to React UI.
904 $net_options = get_site_option( 'cookie_notice_options', [] );
905
906 if ( is_array( $net_options ) && ( ! isset( $net_options['ui_mode'] ) || $net_options['ui_mode'] !== 'react' ) ) {
907 $net_options['ui_mode'] = 'react';
908 update_site_option( 'cookie_notice_options', $net_options );
909 }
910
911 global $wpdb;
912
913 // get all available sites
914 $blogs_ids = $wpdb->get_col( 'SELECT blog_id FROM ' . $wpdb->blogs );
915
916 foreach ( $blogs_ids as $blog_id ) {
917 // change to another site
918 switch_to_blog( (int) $blog_id );
919
920 // run current site activation process
921 $this->activate_site();
922
923 restore_current_blog();
924 }
925 } else {
926 $this->activate_site();
927 // Set transient so maybe_redirect_after_activation() fires on the next admin_init.
928 // Single-site only — network activation handled above (no per-site redirect).
929 set_transient( 'cn_activation_redirect', 1, 30 );
930 }
931 }
932
933 /**
934 * Single site activation.
935 *
936 * @return void
937 */
938 public function activate_site() {
939 // New installs start with React UI via activation_defaults.
940 $activation_defaults = $this->defaults['general'];
941 $activation_defaults['ui_mode'] = 'react';
942
943 add_option( 'cookie_notice_options', $activation_defaults, null, false );
944 add_option( 'cookie_notice_privacy_consent', $this->defaults['privacy_consent'], null, false );
945 add_option( 'cookie_notice_status', $this->defaults['data'], null, false );
946 add_option( 'cookie_notice_version', $this->defaults['version'], null, false );
947
948 // Reactivations: add_option above is a no-op when the key exists,
949 // so explicitly switch existing sites to React UI on activation.
950 $options = get_option( 'cookie_notice_options', [] );
951
952 if ( is_array( $options ) && ( ! isset( $options['ui_mode'] ) || $options['ui_mode'] !== 'react' ) ) {
953 $options['ui_mode'] = 'react';
954 update_option( 'cookie_notice_options', $options );
955 }
956 }
957
958 /**
959 * Redirect to the React admin welcome screen after single-site activation.
960 *
961 * Fires on admin_init. Reads a short-lived transient set by activation().
962 * Guards against: network admin, bulk activation, and insufficient caps.
963 *
964 * ⚠️ Uses cn_react_welcome=1 (NOT welcome=1) — admin-welcome.js intercepts
965 * the ?welcome=1 param and opens the old PHP modal simultaneously if used.
966 *
967 * @return void
968 */
969 public function maybe_redirect_after_activation() {
970 if ( ! get_transient( 'cn_activation_redirect' ) ) {
971 return;
972 }
973
974 // Never redirect inside the network admin screen.
975 if ( is_network_admin() ) {
976 return;
977 }
978
979 // Bulk-activate (wp-admin/plugins.php?activate-multi=true) — skip redirect.
980 if ( isset( $_GET['activate-multi'] ) ) {
981 return;
982 }
983
984 // Only admins should be redirected.
985 if ( ! current_user_can( 'manage_options' ) ) {
986 return;
987 }
988
989 delete_transient( 'cn_activation_redirect' );
990
991 wp_safe_redirect( admin_url( 'admin.php?page=cookie-notice&cn_react_welcome=1' ) );
992 exit;
993 }
994
995 /**
996 * Show a one-time success notice after a React modal license assignment.
997 *
998 * Triggered by ?license_assigned=1 (set by LicenseSelectStep on success).
999 * Reads optional ?slots_remaining=N for copy personalisation.
1000 * The param disappears on the next page load automatically — no transient needed.
1001 *
1002 * @return void
1003 */
1004 public function maybe_show_license_assigned_notice() {
1005 if ( ! is_admin() )
1006 return;
1007
1008 if ( empty( $_GET['license_assigned'] ) || $_GET['license_assigned'] !== '1' )
1009 return;
1010
1011 if ( ! current_user_can( 'manage_options' ) )
1012 return;
1013
1014 $slots = isset( $_GET['slots_remaining'] ) ? (int) $_GET['slots_remaining'] : null;
1015
1016 if ( $slots !== null && $slots > 0 ) {
1017 /* translators: %d: number of remaining domains on the plan */
1018 $slots_text = ' ' . sprintf(
1019 _n( '%d domain remaining on your plan.', '%d domains remaining on your plan.', $slots, 'cookie-notice' ),
1020 $slots
1021 );
1022 } elseif ( $slots === 0 ) {
1023 $slots_text = ' ' . esc_html__( 'No domains remaining on this plan.', 'cookie-notice' );
1024 } else {
1025 $slots_text = '';
1026 }
1027
1028 $message = esc_html__( 'Cookie Compliance — Pro is now active on this site.', 'cookie-notice' ) . $slots_text;
1029
1030 $this->add_notice(
1031 '<p>' . $message . '</p>',
1032 'notice-success is-dismissible'
1033 );
1034 }
1035
1036 /**
1037 * Switch UI mode via ?ui_mode=react|legacy query param.
1038 *
1039 * Persists the choice to the DB so it sticks across page loads.
1040 * Admin-only (manage_options). Works in production — no CN_DEV_MODE required.
1041 *
1042 * @return void
1043 */
1044 public function maybe_switch_ui_mode() {
1045 if ( ! isset( $_GET['ui_mode'] ) )
1046 return;
1047
1048 // Only process ui_mode switches on the plugin's own admin page.
1049 if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'cookie-notice' )
1050 return;
1051
1052 if ( ! current_user_can( 'manage_options' ) )
1053 return;
1054
1055 $requested = sanitize_key( $_GET['ui_mode'] );
1056
1057 if ( ! in_array( $requested, [ 'react', 'legacy' ], true ) )
1058 return;
1059
1060 $current = $this->options['general']['ui_mode'];
1061
1062 // Update DB only if the value actually changed.
1063 if ( $current !== $requested ) {
1064 $this->options['general']['ui_mode'] = $requested;
1065
1066 if ( $this->is_network_admin() ) {
1067 $db_options = get_site_option( 'cookie_notice_options', [] );
1068 } else {
1069 $db_options = get_option( 'cookie_notice_options', [] );
1070 }
1071
1072 $db_options['ui_mode'] = $requested;
1073
1074 if ( $this->is_network_admin() ) {
1075 update_site_option( 'cookie_notice_options', $db_options );
1076 } else {
1077 update_option( 'cookie_notice_options', $db_options );
1078 }
1079 }
1080
1081 // Always set in-memory so the current request renders the correct view.
1082 $this->options['general']['ui_mode'] = $requested;
1083 }
1084
1085 /**
1086 * Plugin deactivation.
1087 *
1088 * @global object $wpdb
1089 *
1090 * @param bool $network
1091 * @return void
1092 */
1093 public function deactivation( $network ) {
1094 // network deactivation?
1095 if ( is_multisite() && $network ) {
1096 $delete = $this->options['general']['global_override'] && $this->options['general']['deactivation_delete'];
1097
1098 // delete network options?
1099 if ( $delete ) {
1100 delete_site_option( 'cookie_notice_options' );
1101 delete_site_option( 'cookie_notice_privacy_consent' );
1102 delete_site_option( 'cookie_notice_status' );
1103 delete_site_option( 'cookie_notice_app_analytics' );
1104 delete_site_option( 'cookie_notice_app_blocking' );
1105 delete_site_option( 'cookie_notice_version' );
1106 }
1107
1108 global $wpdb;
1109
1110 // get all available sites
1111 $blogs_ids = $wpdb->get_col( 'SELECT blog_id FROM ' . $wpdb->blogs );
1112
1113 foreach ( $blogs_ids as $blog_id ) {
1114 // change to another site
1115 switch_to_blog( (int) $blog_id );
1116
1117 // run current site deactivation process
1118 $this->deactivate_site( $delete );
1119
1120 restore_current_blog();
1121 }
1122 } else
1123 $this->deactivate_site();
1124 }
1125
1126 /**
1127 * Single site deactivation.
1128 *
1129 * @param bool $force_deletion
1130 * @return void
1131 */
1132 public function deactivate_site( $force_deletion = false ) {
1133 // delete settings?
1134 if ( $force_deletion || $this->options['general']['deactivation_delete'] ) {
1135 // delete options
1136 delete_option( 'cookie_notice_options' );
1137 delete_option( 'cookie_notice_privacy_consent' );
1138 delete_option( 'cookie_notice_status' );
1139 delete_option( 'cookie_notice_app_analytics' );
1140 delete_option( 'cookie_notice_app_blocking' );
1141 delete_option( 'cookie_notice_version' );
1142
1143 // delete transients if any
1144 delete_transient( 'cookie_notice_app_token' );
1145 delete_transient( 'cookie_notice_app_quick_config' );
1146 delete_transient( 'cookie_notice_app_subscriptions' );
1147 }
1148
1149 // remove wp super cache cookie
1150 $this->wpsc_delete_cookie();
1151 }
1152
1153 /**
1154 * Update notice.
1155 *
1156 * @return void
1157 */
1158 public function update_notice() {
1159 if ( ! current_user_can( 'install_plugins' ) )
1160 return;
1161
1162 // bail an ajax
1163 if ( wp_doing_ajax() )
1164 return;
1165
1166 $network = $this->is_network_admin();
1167
1168 // get cookie compliance status
1169 $status = $this->get_status();
1170
1171 // get subscription
1172 $subscription = $this->get_subscription();
1173
1174 // update number
1175 $current_update = 14;
1176
1177 // new version?
1178 if ( version_compare( $this->db_version, $this->defaults['version'], '<' ) ) {
1179 if ( $this->options['general']['update_version'] < $current_update ) {
1180 // check version, if update version is lower than plugin version, set update notice to true
1181 $this->options['general']['update_version'] = $current_update;
1182 $this->options['general']['update_notice'] = true;
1183
1184 // update options
1185 if ( $network ) {
1186 $this->options['general']['update_notice_diss'] = false;
1187
1188 update_site_option( 'cookie_notice_options', $this->options['general'] );
1189 } else
1190 update_option( 'cookie_notice_options', $this->options['general'] );
1191 }
1192
1193 // update 2.4.17+
1194 if ( version_compare( $this->db_version, '2.4.17', '<' ) ) {
1195 // get cookie compliance activation timestamp
1196 $activation_date = $this->get_cc_activation_datetime();
1197
1198 // get status data
1199 $data = $this->status_data;
1200
1201 // no activation timestamp?
1202 if ( empty( $activation_date ) ) {
1203 if ( $status === 'active' )
1204 $activation = time();
1205 else
1206 $activation = 0;
1207 } else
1208 $activation = (int) $data['activation_datetime'];
1209
1210 // update activation timestamp
1211 $data['activation_datetime'] = $activation;
1212
1213 if ( $network )
1214 update_site_option( 'cookie_notice_status', $data );
1215 else
1216 update_option( 'cookie_notice_status', $data, false );
1217 }
1218
1219 // update plugin version
1220 if ( $network )
1221 update_site_option( 'cookie_notice_version', $this->defaults['version'] );
1222 else
1223 update_option( 'cookie_notice_version', $this->defaults['version'], false );
1224 }
1225
1226 // check page
1227 $page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
1228
1229 // if visiting settings, mark notice as read
1230 if ( $page === 'cookie-notice' && ! empty( $_GET['welcome'] ) ) {
1231 $this->options['general']['update_notice'] = false;
1232
1233 if ( $network ) {
1234 $this->options['general']['update_notice_diss'] = true;
1235
1236 update_site_option( 'cookie_notice_options', $this->options['general'] );
1237 } else
1238 update_option( 'cookie_notice_options', $this->options['general'] );
1239 }
1240
1241 if ( is_multisite() && ( ( $this->is_plugin_network_active() && ! $network && $this->network_options['general']['global_override'] ) || ( $network && ! $this->is_plugin_network_active() ) ) )
1242 $this->options['general']['update_notice'] = false;
1243
1244 // compliance only
1245 if ( $status === 'active' ) {
1246 // get analytics data options
1247 if ( $network )
1248 $analytics = get_site_option( 'cookie_notice_app_analytics', [] );
1249 else
1250 $analytics = get_option( 'cookie_notice_app_analytics', [] );
1251
1252 if ( is_multisite() && ( ( $network && ! $this->is_plugin_network_active() && ! $this->network_options['general']['global_override'] ) || ( ! $network && $this->is_plugin_network_active() && $this->network_options['general']['global_override'] ) ) )
1253 $allow_notice = false;
1254 else
1255 $allow_notice = true;
1256
1257 // show threshold limit warning
1258 if ( ! empty( $analytics ) && $allow_notice ) {
1259 // cycle usage data
1260 $cycle_usage = [
1261 'threshold' => ! empty( $analytics['cycleUsage']->threshold ) ? (int) $analytics['cycleUsage']->threshold : 0,
1262 'end_date' => ! empty( $analytics['cycleUsage']->endDate ) ? date_create_from_format( '!Y-m-d', $analytics['cycleUsage']->endDate ) : date_create_from_format( 'Y-m-d H:i:s', current_time( 'mysql', true ) )
1263 ];
1264
1265 // if threshold in use
1266 if ( $cycle_usage['threshold'] ) {
1267 // Gate on the single derived flag rather than re-deriving from raw
1268 // visits here. This branch used to compare visits >= threshold with no
1269 // plan check at all, so a Pro app whose cached blob still held a free
1270 // app's counters was told its compliance services had been deactivated
1271 // while everything was in fact running (HS#47302). threshold_exceeded()
1272 // is plan-aware and refuses to fire on a snapshot it cannot prove is
1273 // current. The remaining term keeps a dismissed notice dismissed for
1274 // the rest of the cycle.
1275 if ( $this->threshold_exceeded() && $this->options['general']['update_threshold_date'] < $cycle_usage['end_date']->getTimestamp() ) {
1276 $date_format = get_option( 'date_format' );
1277
1278 $upgrade_link = $this->get_url( 'dashboard', '?app-id=' . $this->options['general']['app_id'] . '&open-modal=payment' );
1279 $threshold = $cycle_usage['threshold'];
1280 $cycle_date = date_i18n( $date_format, $cycle_usage['end_date']->getTimestamp() );
1281
1282 $this->add_notice( '<div class="cn-notice-text" data-delay="' . esc_attr( $cycle_usage['end_date']->getTimestamp() ) . '"><h2>' . esc_html__( 'Cookie Compliance Warning', 'cookie-notice') . '</h2><p>' . sprintf( __( 'Your website has reached the <b>%1$s visits usage limit for the Cookie Compliance Free Plan</b>. Compliance services such as Consent Record Storage, Autoblocking, and Consent Analytics have been deactivated until current usage cycle ends on %2$s.', 'cookie-notice' ), $threshold, $cycle_date ) . '<br>' . sprintf( __( 'To reactivate compliance services now, <a href="%s" target="_blank">upgrade your domain to a Pro plan.</a>', 'cookie-notice' ) . '</p></div>', $upgrade_link ), 'cn-threshold error is-dismissible', 'div' );
1283 }
1284 }
1285 }
1286
1287 // display review notice, for multisite only for network admin area
1288 if ( ! empty( $this->options['general']['review_notice'] ) && ( ! is_multisite() || ( $network && $this->is_plugin_network_active() ) ) ) {
1289 // get current time
1290 $current_time = time();
1291
1292 // get cookie compliance activation timestamp
1293 $activation_date = $this->get_cc_activation_datetime();
1294
1295 // get delay timestamp
1296 $delay_timestamp = (int) $this->options['general']['review_notice_delay'];
1297
1298 // no delay?
1299 if ( $delay_timestamp === 0 )
1300 $compare_timestamp = $activation_date + 2 * WEEK_IN_SECONDS;
1301 else
1302 $compare_timestamp = $delay_timestamp;
1303
1304 // display notice?
1305 if ( $compare_timestamp < $current_time )
1306 $this->add_notice( '<div class="cn-notice-text cn-review"><h2>' . esc_html__( 'We Value Your Feedback', 'cookie-notice' ) . '</h2><p>' . sprintf( __( "Hi, you've been using <strong>Cookie Compliance</strong> for more than %s. We hope it has been a valuable addition to your WordPress site. We would be grateful if you could take a few minutes to share your thoughts by leaving a review.", 'cookie-notice' ), human_time_diff( $activation_date, $current_time ) ) . '<br>' . esc_html__( 'Thank you for helping us improve and grow!', 'cookie-notice' ) . '</p><p class="cn-notice-actions"><a href="https://wordpress.org/support/plugin/cookie-notice/reviews/?filter=5#new-post" class="button-link cn-notice-review" target="_blank" rel="noopener">' . esc_html__( 'Review', 'cookie-notice' ) . '</a><a href="#" class="button-link cn-notice-delay">' . esc_html__( 'Delay', 'cookie-notice' ) . '</a><a href="#" class="button-link cn-notice-dismiss">' . esc_html__( 'Dismiss', 'cookie-notice' ) . '</a></p></div>', 'error', 'div' );
1307 }
1308 }
1309 }
1310
1311 /**
1312 * Add admin notice.
1313 *
1314 * @param string $html
1315 * @param string $status
1316 * @param string $container
1317 * @return void
1318 */
1319 private function add_notice( $html = '', $status = 'error', $container = '' ) {
1320 $this->notices[] = [
1321 'html' => $html,
1322 'status' => $status,
1323 'container' => ( ! empty( $container ) && in_array( $container, [ 'p', 'div' ] ) ? $container : '' )
1324 ];
1325
1326 add_action( 'admin_notices', [ $this, 'display_notice' ], 0 );
1327 add_action( 'network_admin_notices', [ $this, 'display_notice' ], 0 );
1328 }
1329
1330 /**
1331 * Print admin notices.
1332 *
1333 * @return void
1334 */
1335 public function display_notice() {
1336 foreach( $this->notices as $notice ) {
1337 echo '
1338 <div id="cn-admin-notice" class="cn-notice notice notice-info ' . esc_attr( $notice['status'] ) . '">
1339 ' . ( ! empty( $notice['container'] ) ? '<' . esc_attr( $notice['container'] ) . ' class="cn-notice-container">' : '' ) . '
1340 ' . wp_kses_post( $notice['html'] ) . '
1341 ' . ( ! empty( $notice['container'] ) ? '</' . esc_attr( $notice['container'] ) . ' class="cn-notice-container">' : '' ) . '
1342 </div>';
1343 }
1344 }
1345
1346 /**
1347 * Dismiss admin notice.
1348 *
1349 * @return void
1350 */
1351 public function ajax_dismiss_admin_notice() {
1352 if ( ! current_user_can( 'install_plugins' ) )
1353 exit;
1354
1355 if ( ! isset( $_POST['nonce'], $_POST['notice_action'] ) )
1356 exit;
1357
1358 if ( wp_verify_nonce( $_POST['nonce'], 'cn_dismiss_notice' ) ) {
1359 // get notice action
1360 $notice_action = ! empty( $_POST['notice_action'] ) ? sanitize_key( $_POST['notice_action'] ) : 'dismiss';
1361
1362 $cn_network = isset( $_POST['cn_network'] ) ? (int) $_POST['cn_network'] : false;
1363
1364 // network?
1365 $network = is_multisite() && $cn_network === 1;
1366
1367 switch ( $notice_action ) {
1368 // threshold notice
1369 case 'threshold':
1370 // set delay period last cycle day
1371 $delay = isset( $_POST['param'] ) ? (int) $_POST['param'] : 0;
1372
1373 $this->options['general']['update_threshold_date'] = $delay + DAY_IN_SECONDS;
1374
1375 // update options
1376 if ( $network )
1377 update_site_option( 'cookie_notice_options', $this->options['general'] );
1378 else
1379 update_option( 'cookie_notice_options', $this->options['general'] );
1380 break;
1381
1382 // delay notice
1383 case 'delay':
1384 // set delay period to 2 weeks from now
1385 $this->options['general']['update_delay_date'] = time() + 2 * WEEK_IN_SECONDS;
1386
1387 // update options
1388 if ( $network )
1389 update_site_option( 'cookie_notice_options', $this->options['general'] );
1390 else
1391 update_option( 'cookie_notice_options', $this->options['general'] );
1392 break;
1393
1394 // hide notice
1395 case 'approve':
1396 default:
1397 $this->options['general']['update_notice'] = false;
1398 $this->options['general']['update_delay_date'] = 0;
1399
1400 // update options
1401 if ( $network ) {
1402 $this->options['general']['update_notice_diss'] = true;
1403
1404 update_site_option( 'cookie_notice_options', $this->options['general'] );
1405 } else
1406 update_option( 'cookie_notice_options', $this->options['general'] );
1407 }
1408 }
1409
1410 exit;
1411 }
1412
1413 /**
1414 * Dismiss review admin notice.
1415 *
1416 * @return void
1417 */
1418 public function ajax_review_notice() {
1419 if ( ! current_user_can( 'install_plugins' ) )
1420 exit;
1421
1422 if ( ! isset( $_POST['nonce'], $_POST['notice_action'] ) )
1423 exit;
1424
1425 if ( wp_verify_nonce( $_POST['nonce'], 'cn_review_notice' ) ) {
1426 // get notice action
1427 $notice_action = ! empty( $_POST['notice_action'] ) ? sanitize_key( $_POST['notice_action'] ) : 'dismiss';
1428
1429 $cn_network = isset( $_POST['cn_network'] ) ? (int) $_POST['cn_network'] : false;
1430
1431 // network?
1432 $network = is_multisite() && $cn_network === 1;
1433
1434 switch ( $notice_action ) {
1435 // delay notice
1436 case 'delay':
1437 $this->options['general']['review_notice'] = true;
1438 $this->options['general']['review_notice_delay'] = time() + 2 * WEEK_IN_SECONDS;
1439
1440 // update options
1441 if ( $network )
1442 update_site_option( 'cookie_notice_options', $this->options['general'] );
1443 else
1444 update_option( 'cookie_notice_options', $this->options['general'] );
1445 break;
1446
1447 // hide notice
1448 case 'dismiss':
1449 case 'review':
1450 default:
1451 $this->options['general']['review_notice'] = false;
1452 $this->options['general']['review_notice_delay'] = 0;
1453
1454 // update options
1455 if ( $network ) {
1456 $this->options['general']['update_notice_diss'] = true;
1457
1458 update_site_option( 'cookie_notice_options', $this->options['general'] );
1459 } else
1460 update_option( 'cookie_notice_options', $this->options['general'] );
1461 }
1462 }
1463
1464 exit;
1465 }
1466
1467 /**
1468 * Register shortcode.
1469 *
1470 * @return void
1471 */
1472 public function register_shortcodes() {
1473 add_shortcode( 'cookies_accepted', [ $this, 'cookies_accepted_shortcode' ] );
1474 add_shortcode( 'cookies_revoke', [ $this, 'cookies_revoke_shortcode' ] );
1475 add_shortcode( 'cookies_policy_link', [ $this, 'cookies_policy_link_shortcode' ] );
1476 }
1477
1478 /**
1479 * Register cookies accepted shortcode.
1480 *
1481 * @param array $args
1482 * @param string $content
1483 * @return string
1484 */
1485 public function cookies_accepted_shortcode( $args, $content ) {
1486 if ( $this->cookies_accepted() ) {
1487 // Only sanitize with wp_kses - do not decode entities from user-generated content
1488 $scripts = trim( wp_kses( $content, $this->get_allowed_html( 'body' ) ) );
1489
1490 if ( ! empty( $scripts ) ) {
1491 if ( preg_match_all( '/' . get_shortcode_regex() . '/', $content ) )
1492 $scripts = do_shortcode( $scripts );
1493
1494 return $scripts;
1495 }
1496 }
1497
1498 return '';
1499 }
1500
1501 /**
1502 * Register cookies revoke shortcode.
1503 *
1504 * @param array $args
1505 * @param string $content
1506 * @return string
1507 */
1508 public function cookies_revoke_shortcode( $args, $content ) {
1509 // get options
1510 $options = $this->options['general'];
1511
1512 // WPML >= 3.2
1513 if ( defined( 'ICL_SITEPRESS_VERSION' ) && version_compare( ICL_SITEPRESS_VERSION, '3.2', '>=' ) )
1514 $options['revoke_text'] = apply_filters( 'wpml_translate_single_string', $options['revoke_text'], 'Cookie Notice', 'Revoke button text' );
1515 // WPML and Polylang compatibility
1516 elseif ( function_exists( 'icl_t' ) )
1517 $options['revoke_text'] = icl_t( 'Cookie Notice', 'Revoke button text', $options['revoke_text'] );
1518
1519 // defaults
1520 $defaults = [
1521 'title' => $options['revoke_text'],
1522 'class' => $options['css_class']
1523 ];
1524
1525 // combine shortcode arguments
1526 $args = shortcode_atts( $defaults, $args );
1527
1528 if ( Cookie_Notice()->get_status() === 'active' )
1529 $shortcode = '<a href="#" class="cn-revoke-cookie cn-button-inline cn-revoke-inline' . esc_attr( $args['class'] !== '' ? ' ' . $args['class'] : '' ) . '" title="' . esc_attr( $args['title'] ) . '" data-hu-action="cookies-notice-revoke">' . esc_html( $args['title'] ) . '</a>';
1530 else
1531 $shortcode = '<a href="#" class="cn-revoke-cookie cn-button-inline cn-revoke-inline' . esc_attr( $args['class'] !== '' ? ' ' . $args['class'] : '' ) . '" title="' . esc_attr( $args['title'] ) . '">' . esc_html( $args['title'] ) . '</a>';
1532
1533 return $shortcode;
1534 }
1535
1536 /**
1537 * Register cookies policy link shortcode.
1538 *
1539 * @param array $args
1540 * @param string $content
1541 * @return string
1542 */
1543 public function cookies_policy_link_shortcode( $args, $content ) {
1544 // get options
1545 $options = $this->options['general'];
1546
1547 // WPML >= 3.2
1548 if ( defined( 'ICL_SITEPRESS_VERSION' ) && version_compare( ICL_SITEPRESS_VERSION, '3.2', '>=' ) ) {
1549 $options['see_more_opt']['text'] = apply_filters( 'wpml_translate_single_string', $options['see_more_opt']['text'], 'Cookie Notice', 'Privacy policy text' );
1550 $options['see_more_opt']['link'] = apply_filters( 'wpml_translate_single_string', $options['see_more_opt']['link'], 'Cookie Notice', 'Custom link' );
1551 // WPML and Polylang compatibility
1552 } elseif ( function_exists( 'icl_t' ) ) {
1553 $options['see_more_opt']['text'] = icl_t( 'Cookie Notice', 'Privacy policy text', $options['see_more_opt']['text'] );
1554 $options['see_more_opt']['link'] = icl_t( 'Cookie Notice', 'Custom link', $options['see_more_opt']['link'] );
1555 }
1556
1557 if ( $options['see_more_opt']['link_type'] === 'page' ) {
1558 // multisite with global override?
1559 if ( is_multisite() && $this->is_plugin_network_active() && $this->network_options['general']['global_override'] ) {
1560 // get main site id
1561 $main_site_id = get_main_site_id();
1562
1563 // switch to main site
1564 switch_to_blog( $main_site_id );
1565
1566 // update page id for current language if needed
1567 if ( function_exists( 'icl_object_id' ) )
1568 $options['see_more_opt']['id'] = icl_object_id( $options['see_more_opt']['id'], 'page', true );
1569
1570 // get main site privacy policy link
1571 $permalink = get_permalink( $options['see_more_opt']['id'] );
1572
1573 // restore current site
1574 restore_current_blog();
1575 } else {
1576 // update page id for current language if needed
1577 if ( function_exists( 'icl_object_id' ) )
1578 $options['see_more_opt']['id'] = icl_object_id( $options['see_more_opt']['id'], 'page', true );
1579
1580 // get privacy policy link
1581 $permalink = get_permalink( $options['see_more_opt']['id'] );
1582 }
1583 }
1584
1585 // defaults
1586 $defaults = [
1587 'title' => $options['see_more_opt']['text'] !== '' ? $options['see_more_opt']['text'] : '&#x279c;',
1588 'link' => $options['see_more_opt']['link_type'] === 'custom' ? $options['see_more_opt']['link'] : $permalink,
1589 'class' => $options['css_class']
1590 ];
1591
1592 // combine shortcode arguments
1593 $args = shortcode_atts( $defaults, $args );
1594
1595 $shortcode = '<a href="' . esc_url( $args['link'] ) . '" target="' . esc_attr( $options['link_target'] ) . '" id="cn-more-info" class="cn-privacy-policy-link cn-link' . esc_attr( $args['class'] !== '' ? ' ' . $args['class'] : '' ) . '" data-link-url="' . esc_url( $args['link'] ) . '" data-link-target="' . esc_attr( $options['link_target'] ) . '">' . esc_html( $args['title'] ) . '</a>';
1596
1597 return $shortcode;
1598 }
1599
1600 /**
1601 * Check if cookies are accepted.
1602 *
1603 * @return bool
1604 */
1605 public static function cookies_accepted() {
1606 if ( Cookie_Notice()->get_status() === 'active' ) {
1607 // get cookie
1608 $cookies = isset( $_COOKIE['hu-consent'] ) ? json_decode( stripslashes( $_COOKIE['hu-consent'] ), true ) : [];
1609
1610 // valid cookie?
1611 if ( json_last_error() === JSON_ERROR_NONE && ! empty( $cookies ) && is_array( $cookies ) && isset( $cookies['consent'] ) )
1612 $result = (bool) $cookies['consent'];
1613 else
1614 $result = false;
1615 } else
1616 $result = isset( $_COOKIE['cookie_notice_accepted'] ) && $_COOKIE['cookie_notice_accepted'] === 'true';
1617
1618 return (bool) apply_filters( 'cn_is_cookie_accepted', $result );
1619 }
1620
1621 /**
1622 * Check if cookies are set.
1623 *
1624 * @return bool
1625 */
1626 public static function cookies_set() {
1627 if ( Cookie_Notice()->get_status() === 'active' )
1628 $result = isset( $_COOKIE['hu-consent'] );
1629 else
1630 $result = isset( $_COOKIE['cookie_notice_accepted'] );
1631
1632 return (bool) apply_filters( 'cn_is_cookie_set', $result );
1633 }
1634
1635 /**
1636 * Add WP Super Cache cookie.
1637 *
1638 * @return void
1639 */
1640 public function wpsc_add_cookie() {
1641 if ( $this->get_status() !== 'active' )
1642 do_action( 'wpsc_add_cookie', 'cookie_notice_accepted' );
1643 }
1644
1645 /**
1646 * Delete WP Super Cache cookie.
1647 *
1648 * @return void
1649 */
1650 public function wpsc_delete_cookie() {
1651 if ( $this->get_status() !== 'active' )
1652 do_action( 'wpsc_delete_cookie', 'cookie_notice_accepted' );
1653 }
1654
1655 /**
1656 * Enqueue admin scripts and styles.
1657 *
1658 * @param string $page
1659 * @return void
1660 */
1661 public function admin_enqueue_scripts( $page ) {
1662 // plugins page?
1663 if ( $page === 'plugins.php' ) {
1664 add_thickbox();
1665
1666 wp_enqueue_script( 'cookie-notice-admin-plugins', COOKIE_NOTICE_URL . '/js/admin-plugins.js', [ 'jquery' ], $this->defaults['version'] );
1667
1668 wp_enqueue_style( 'cookie-notice-admin-plugins', COOKIE_NOTICE_URL . '/css/admin-plugins.css', [], $this->defaults['version'] );
1669
1670 // prepare script data
1671 $script_data = [
1672 'deactivate' => esc_html__( 'Cookie Compliance - Deactivation survey', 'cookie-notice' ),
1673 'nonce' => wp_create_nonce( 'cn-deactivate-plugin' )
1674 ];
1675
1676 wp_add_inline_script( 'cookie-notice-admin-plugins', 'var cnArgsPlugins = ' . wp_json_encode( $script_data ) . ";\n", 'before' );
1677 }
1678
1679 // notice js and css
1680 wp_enqueue_script( 'cookie-notice-admin-notice', COOKIE_NOTICE_URL . '/js/admin-notice.js', [], $this->defaults['version'] );
1681
1682 // prepare script data
1683 $script_data = [
1684 'ajaxURL' => admin_url( 'admin-ajax.php' ),
1685 'nonce' => wp_create_nonce( 'cn_dismiss_notice' ),
1686 'reviewNonce' => wp_create_nonce( 'cn_review_notice' ),
1687 'network' => $this->is_network_admin()
1688 ];
1689
1690 wp_add_inline_script( 'cookie-notice-admin-notice', 'var cnArgsNotice = ' . wp_json_encode( $script_data ) . ";\n", 'before' );
1691
1692 wp_enqueue_style( 'cookie-notice-admin-notice', COOKIE_NOTICE_URL . '/css/admin-notice.css', [], $this->defaults['version'] );
1693 }
1694
1695 /**
1696 * Set plugin links.
1697 *
1698 * @return void
1699 */
1700 public function set_plugin_links() {
1701 // filters
1702 add_filter( 'plugin_action_links', [ $this, 'plugin_action_links' ], 10, 2 );
1703 add_filter( 'network_admin_plugin_action_links', [ $this, 'plugin_action_links' ], 10, 2 );
1704 }
1705
1706 /**
1707 * Add links to settings page.
1708 *
1709 * @param array $links
1710 * @param string $file
1711 * @return array
1712 */
1713 public function plugin_action_links( $links, $file ) {
1714 if ( ! current_user_can( apply_filters( 'cn_manage_cookie_notice_cap', 'manage_options' ) ) )
1715 return $links;
1716
1717 if ( $file === COOKIE_NOTICE_BASENAME ) {
1718 if ( ! empty( $links['deactivate'] ) ) {
1719 // link already contains class attribute?
1720 if ( preg_match( '/<a.*?class=(\'|")(.*?)(\'|").*?>/is', $links['deactivate'], $result ) === 1 )
1721 $links['deactivate'] = preg_replace( '/(<a.*?class=(?:\'|").*?)((?:\'|").*?>)/s', '$1 cn-deactivate-plugin-modal$2', $links['deactivate'] );
1722 else
1723 $links['deactivate'] = preg_replace( '/(<a.*?)>/s', '$1 class="cn-deactivate-plugin-modal">', $links['deactivate'] );
1724
1725 // link already contains href attribute?
1726 if ( preg_match( '/<a.*?href=(\'|")(.*?)(\'|").*?>/is', $links['deactivate'], $result ) === 1 ) {
1727 if ( ! empty( $result[2] ) )
1728 $this->deactivaion_url = $result[2];
1729 }
1730 }
1731
1732 // skip settings link if plugin is activated from main site
1733 if ( ! ( $this->is_network_admin() && ! $this->is_plugin_network_active() ) ) {
1734 $url = $this->is_network_admin() ? network_admin_url( 'admin.php?page=cookie-notice' ) : admin_url( 'admin.php?page=cookie-notice' );
1735
1736 // put settings link at start
1737 array_unshift( $links, sprintf( '<a href="%s">%s</a>', esc_url( $url ), esc_html__( 'Settings', 'cookie-notice' ) ) );
1738 }
1739
1740 // get cookie compliance status
1741 $status = $this->get_status();
1742
1743 if ( is_multisite() ) {
1744 $check_status = empty( $status ) && ( ( $this->is_network_admin() && $this->is_plugin_network_active() && $this->network_options['general']['global_override'] ) || ( ! $this->is_network_admin() && ( ( $this->is_plugin_network_active() && ! $this->network_options['general']['global_override'] ) || ! $this->is_plugin_network_active() ) ) );
1745 } else
1746 $check_status = empty( $status );
1747
1748 // add upgrade link
1749 if ( $check_status ) {
1750 $url = $this->is_network_admin() ? network_admin_url( 'admin.php?page=cookie-notice&welcome=1' ) : admin_url( 'admin.php?page=cookie-notice&welcome=1' );
1751
1752 $links[] = sprintf( '<a href="%s" style="color: #20C19E; font-weight: bold">%s</a>', esc_url( $url ), esc_html__( 'Try Cookie Compliance free', 'cookie-notice' ) );
1753 }
1754 }
1755
1756 return $links;
1757 }
1758
1759 /**
1760 * Deactivation modal HTML template.
1761 *
1762 * @global string $pagenow
1763 *
1764 * @return void
1765 */
1766 public function deactivate_plugin_template() {
1767 global $pagenow;
1768
1769 // display only for plugins page
1770 if ( $pagenow !== 'plugins.php' )
1771 return;
1772
1773 echo '
1774 <div id="cn-deactivation-modal" style="display: none">
1775 <div id="cn-deactivation-container">
1776 <div id="cn-deactivation-body">
1777 <div class="cn-deactivation-options">
1778 <p><em>' . esc_html__( "We're sorry to see you go. Could you please tell us what happened?", 'cookie-notice' ) . '</em></p>
1779 <ul>';
1780
1781 foreach ( [
1782 '1' => esc_html__( "I couldn't figure out how to make it work.", 'cookie-notice' ),
1783 '2' => esc_html__( 'I found another plugin to use for the same task.', 'cookie-notice' ),
1784 '3' => esc_html__( 'The Cookie Compliance banner is too big.', 'cookie-notice' ),
1785 '4' => esc_html__( 'The Cookie Compliance consent choices (Silver, Gold, Platinum) are confusing.', 'cookie-notice' ),
1786 '5' => esc_html__( 'The Cookie Compliance default settings are too strict.', 'cookie-notice' ),
1787 '6' => esc_html__( 'The web application user interface is not clear to me.', 'cookie-notice' ),
1788 '7' => esc_html__( "Support isn't timely.", 'cookie-notice' ),
1789 '8' => esc_html__( 'Other', 'cookie-notice' )
1790 ] as $option => $text ) {
1791 echo '
1792 <li><label><input type="radio" name="cn_deactivation_option" value="' . esc_attr( $option ) . '" ' . checked( '8', $option, false ) . ' />' . esc_html( $text ) . '</label></li>';
1793 }
1794
1795 echo '
1796 </ul>
1797 </div>
1798 <div class="cn-deactivation-textarea">
1799 <textarea name="cn_deactivation_other"></textarea>
1800 </div>
1801 </div>
1802 <div id="cn-deactivation-footer">
1803 <a href="" class="button cn-deactivate-plugin-cancel">' . esc_html__( 'Cancel', 'cookie-notice' ) . '</a>
1804 <a href="' . esc_url( $this->deactivaion_url ) . '" class="button button-secondary cn-deactivate-plugin-simple">' . esc_html__( 'Deactivate', 'cookie-notice' ) . '</a>
1805 <a href="' . esc_url( $this->deactivaion_url ) . '" class="button button-primary right cn-deactivate-plugin-data">' . esc_html__( 'Deactivate & Submit', 'cookie-notice' ) . '</a>
1806 <span class="spinner"></span>
1807 </div>
1808 </div>
1809 </div>';
1810 }
1811
1812 /**
1813 * Send data about deactivation of the plugin.
1814 *
1815 * @return void
1816 */
1817 public function deactivate_plugin() {
1818 // check permissions
1819 if ( ! current_user_can( 'install_plugins' ) || wp_verify_nonce( $_POST['nonce'], 'cn-deactivate-plugin' ) === false )
1820 return;
1821
1822 if ( isset( $_POST['option_id'] ) ) {
1823 $option_id = (int) $_POST['option_id'];
1824
1825 // avoid fake submissions
1826 if ( $option_id === 8 ) {
1827 $other = isset( $_POST['other'] ) ? sanitize_textarea_field( $_POST['other'] ) : '';
1828
1829 // no reason?
1830 if ( $other === '' )
1831 wp_send_json_success();
1832 }
1833
1834 wp_remote_post(
1835 'https://hu-manity.co/wp-json/api/v1/forms/',
1836 [
1837 'timeout' => 15,
1838 'blocking' => true,
1839 'headers' => [],
1840 'body' => [
1841 'id' => 1,
1842 'option' => $option_id,
1843 'other' => $other,
1844 'referrer' => get_site_url()
1845 ]
1846 ]
1847 );
1848
1849 wp_send_json_success();
1850 }
1851
1852 wp_send_json_error();
1853 }
1854
1855 /**
1856 * Get allowed script blocking HTML.
1857 *
1858 * @param string $type
1859 * @return array
1860 */
1861 public function get_allowed_html( $type = 'head' ) {
1862 // default allowed html for both types
1863 $allowed_html = [
1864 'script' => [
1865 'type' => true,
1866 'src' => true,
1867 'charset' => true,
1868 'async' => true,
1869 'defer' => true,
1870 'crossorigin' => true,
1871 'fetchpriority' => true,
1872 'referrerpolicy' => true,
1873 'nomodule' => true,
1874 'nonce' => true,
1875 'integrity' => true,
1876 'class' => true,
1877 'id' => true
1878 ],
1879 'noscript' => [
1880 'class' => true,
1881 'id' => true
1882 ],
1883 'style' => [
1884 'type' => true,
1885 'media' => true,
1886 'nonce' => true,
1887 'class' => true,
1888 'id' => true
1889 ]
1890 ];
1891
1892 if ( $type === 'head' ) {
1893 // allow links for head
1894 $allowed_html['link'] = [
1895 'as' => true,
1896 'crossorigin' => true,
1897 'fetchpriority' => true,
1898 'imagesizes' => true,
1899 'imagesrcset' => true,
1900 'referrerpolicy' => true,
1901 'sizes' => true,
1902 'integrity' => true,
1903 'href' => true,
1904 'hreflang' => true,
1905 'rel' => true,
1906 'type' => true,
1907 'title' => true,
1908 'media' => true,
1909 'class' => true,
1910 'id' => true
1911 ];
1912 } elseif ( $type === 'body' ) {
1913 // allow ifarmes for body
1914 $allowed_html['iframe'] = [
1915 'src' => true,
1916 'srcdoc' => true,
1917 'height' => true,
1918 'width' => true,
1919 'class' => true,
1920 'id' => true,
1921 'allow' => true,
1922 'loading' => true,
1923 'name' => true,
1924 'title' => true,
1925 'referrerpolicy' => true,
1926 'sandbox' => true,
1927 'allowfullscreen' => true
1928 ];
1929 }
1930
1931 // combine allowed tags with default post allowed tags
1932 return apply_filters( 'cn_refuse_code_allowed_html', array_merge( wp_kses_allowed_html( 'post' ), $allowed_html ), $type );
1933 }
1934
1935 /**
1936 * Merge multidimensional associative arrays.
1937 * Works only with strings, integers and arrays as keys. Values can be any type but they have to have same type to be kept in the final array.
1938 * Every array should have the same type of elements. Only keys from $defaults array will be kept in the final array unless $siblings are not empty.
1939 * $siblings examples: array( '=>', 'only_first_level', 'first_level=>second_level', 'first_key=>next_key=>sibling' ) and so on.
1940 * Single '=>' means that all siblings of the highest level will be kept in the final array.
1941 *
1942 * @param array $defaults Array with defaults values
1943 * @param array $array Array to merge
1944 * @param bool|array $siblings Whether to allow "string" siblings to copy from $array if they do not exist in $defaults, false otherwise
1945 * @return array
1946 */
1947 public function multi_array_merge( $defaults, $array, $siblings = false ) {
1948 // make a copy for better performance and to prevent $default override in foreach
1949 $copy = $defaults;
1950
1951 // prepare siblings for recursive deeper level
1952 $new_siblings = [];
1953
1954 // allow siblings?
1955 if ( ! empty( $siblings ) && is_array( $siblings ) ) {
1956 foreach ( $siblings as $sibling ) {
1957 // highest level siblings
1958 if ( $sibling === '=>' ) {
1959 // copy all non-existent string siblings
1960 foreach( $array as $key => $value ) {
1961 if ( is_string( $key ) && ! array_key_exists( $key, $defaults ) ) {
1962 $defaults[$key] = null;
1963 }
1964 }
1965 // sublevel siblings
1966 } else {
1967 // explode siblings
1968 $ex = explode( '=>', $sibling );
1969
1970 // copy all non-existent siblings
1971 foreach ( array_keys( $array[$ex[0]] ) as $key ) {
1972 if ( ! array_key_exists( $key, $defaults[$ex[0]] ) )
1973 $defaults[$ex[0]][$key] = null;
1974 }
1975
1976 // more than one sibling child?
1977 if ( count( $ex ) > 1 )
1978 $new_siblings[$ex[0]] = [ substr_replace( $sibling, '', 0, strlen( $ex[0] . '=>' ) ) ];
1979 // no more sibling children
1980 else
1981 $new_siblings[$ex[0]] = false;
1982 }
1983 }
1984 }
1985
1986 // loop through first array
1987 foreach ( $defaults as $key => $value ) {
1988 // integer key?
1989 if ( is_int( $key ) ) {
1990 $copy = array_unique( array_merge( $defaults, $array ), SORT_REGULAR );
1991
1992 break;
1993 // string key?
1994 } elseif ( is_string( $key ) && isset( $array[$key] ) ) {
1995 // string, boolean, integer or null values?
1996 if ( ( is_string( $value ) && is_string( $array[$key] ) ) || ( is_bool( $value ) && is_bool( $array[$key] ) ) || ( is_int( $value ) && is_int( $array[$key] ) ) || is_null( $value ) )
1997 $copy[$key] = $array[$key];
1998 // arrays
1999 elseif ( is_array( $value ) && isset( $array[$key] ) && is_array( $array[$key] ) ) {
2000 if ( empty( $value ) )
2001 $copy[$key] = $array[$key];
2002 else
2003 $copy[$key] = $this->multi_array_merge( $defaults[$key], $array[$key], ( isset( $new_siblings[$key] ) ? $new_siblings[$key] : false ) );
2004 }
2005 }
2006 }
2007
2008 return $copy;
2009 }
2010
2011 /**
2012 * Indicate if current page is the Cookie Policy page.
2013 *
2014 * @return bool
2015 */
2016 public function is_cookie_policy_page() {
2017 // get privacy policy options
2018 $see_more = $this->options['general']['see_more_opt'];
2019
2020 // custom link?
2021 if ( $see_more['link_type'] !== 'page' )
2022 return false;
2023
2024 // get current object
2025 $current_page = sanitize_post( $GLOBALS['wp_the_query']->get_queried_object() );
2026
2027 // check if current page is privacy policy page
2028 return $current_page->post_name === get_post_field( 'post_name', $see_more['id'] );
2029 }
2030 }
2031
2032 /**
2033 * Initialize Cookie Notice.
2034 *
2035 * @return object
2036 */
2037 function Cookie_Notice() {
2038 static $instance;
2039
2040 // first call to instance() initializes the plugin
2041 if ( $instance === null || ! ( $instance instanceof Cookie_Notice ) )
2042 $instance = Cookie_Notice::instance();
2043
2044 return $instance;
2045 }
2046
2047 Cookie_Notice();
2048