cache_plugin_compat(); if ( class_exists( 'CookieBanerPro_Updater' ) ) { CookieBanerPro_Updater::init(); } } /** Base address of the CookieBanerPro service (filterable for staging). */ public static function service() { return untrailingslashit( apply_filters( 'cookiebanerpro_service_url', 'https://cookiebanerpro.pl' ) ); } public static function is_connected() { return (bool) get_option( self::OPT_KEY ) && (bool) get_option( self::OPT_TOKEN ); } /** * Defaults a theme can ship for its buyers, so the banner matches the theme from the first minute: * add_theme_support( 'cookiebanerpro', array( 'partner' => 'ABC123', 'colors' => array( 'primary' => '#1E6B52', 'background' => '#FFFFFF', 'text' => '#1E2B2F' ), 'style' => 'box' ) ); * or the filter 'cookiebanerpro_theme_defaults'. Keys: partner (code from the CookieBanerPro Partner Panel), * colors (primary, background, text), style (box|bar|popup), dark (bool). */ public static function theme_defaults() { $support = get_theme_support( 'cookiebanerpro' ); $defaults = ( is_array( $support ) && isset( $support[0] ) && is_array( $support[0] ) ) ? $support[0] : array(); $defaults = apply_filters( 'cookiebanerpro_theme_defaults', $defaults ); return is_array( $defaults ) ? $defaults : array(); } /** True when the active theme bundles CookieBanerPro (theme support or the defaults filter). */ public static function theme_bundled() { return current_theme_supports( 'cookiebanerpro' ) || array() !== self::theme_defaults(); } /** Language of the banner texts from the site locale (pl, en, de, uk). */ public static function banner_lang() { $locale = strtolower( substr( (string) get_locale(), 0, 2 ) ); return in_array( $locale, array( 'pl', 'en', 'de', 'uk' ), true ) ? $locale : 'en'; } /** Plan limit alerts on the dashboard and the Plugins screen (status is cached, so no extra requests). */ public function limit_notice() { if ( ! self::is_connected() || ! current_user_can( 'manage_options' ) ) { return; } $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || ! in_array( $screen->id, array( 'dashboard', 'plugins' ), true ) ) { return; } $status = self::status(); if ( ! is_array( $status ) || isset( $status['_error'] ) || empty( $status['pageviews_limit'] ) ) { return; } $pv = (int) $status['pageviews']; $limit = (int) $status['pageviews_limit']; if ( $pv < 0.8 * $limit ) { return; } $over = $pv >= $limit; $text = $over ? __( 'your site has used up the page views of its plan this month. Choose a plan so the banner keeps working - on the free plan it switches off 7 days after the limit.', 'cookiebanerpro' ) : __( 'your site has used 80% of the page views of its plan this month.', 'cookiebanerpro' ); printf( '

CookieBanerPro: %2$s %4$s

', $over ? 'error' : 'warning', esc_html( $text ), esc_url( self::page_url() ), esc_html__( 'See your plan', 'cookiebanerpro' ) ); } /* -------------------------------------------------------------------- * Front end: banner code in , as early as possible * ------------------------------------------------------------------ */ public static function site_key() { $key = (string) get_option( self::OPT_KEY ); return preg_match( '/^[a-z0-9]{12}$/', $key ) ? $key : ''; } public function print_banner_code() { if ( is_admin() || is_feed() || is_robots() || is_trackback() ) { return; } if ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) { return; } if ( '1' !== (string) get_option( self::OPT_INJECT, '1' ) ) { return; } $key = self::site_key(); if ( '' === $key ) { return; } $src = self::service() . '/c/' . $key . '.js'; // Same code as the manual installation: Google Consent Mode v2 defaults first, then the banner loader. // data-* attributes tell optimisation plugins (Cloudflare Rocket Loader, LiteSpeed, WP Rocket...) to leave it alone. $skip = array( 'data-cfasync' => 'false', 'data-no-optimize' => '1', 'data-no-defer' => '1', 'data-no-minify' => '1', ); wp_print_inline_script_tag( "window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('consent','default',{ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied',analytics_storage:'denied',functionality_storage:'denied',personalization_storage:'denied',security_storage:'granted',wait_for_update:500});", $skip + array( 'id' => 'cookiebanerpro-consent-default', 'data-cookieconsent' => 'ignore' ) ); // Early blocking (default): the banner loads before the rest of the page (no async), so embedded YouTube videos // and tracking scripts wait for consent from the very first view. With async the page may load them first. $early = '1' === (string) get_option( self::OPT_EARLY, '1' ); wp_print_script_tag( array( 'src' => esc_url( $src ), 'async' => ! $early, 'id' => 'cookiebanerpro-banner' ) + $skip ); } /** * Popular optimisation plugins delay or combine JavaScript, which breaks consent banners * (the banner shows late or Consent Mode defaults arrive after the tags). Exclude our code. */ private function cache_plugin_compat() { $patterns = array( 'cookiebanerpro', 'gtag(\'consent\'', "gtag('consent'" ); $add = function ( $list ) use ( $patterns ) { return array_values( array_unique( array_merge( (array) $list, $patterns ) ) ); }; // WP Rocket. add_filter( 'rocket_delay_js_exclusions', $add ); add_filter( 'rocket_exclude_js', $add ); add_filter( 'rocket_exclude_defer_js', $add ); add_filter( 'rocket_excluded_inline_js_content', $add ); // LiteSpeed Cache. add_filter( 'litespeed_optm_js_defer_exc', $add ); add_filter( 'litespeed_optimize_js_excludes', $add ); add_filter( 'litespeed_optm_gm_js_exc', $add ); // SiteGround Optimizer. add_filter( 'sgo_javascript_combine_exclude', $add ); add_filter( 'sgo_js_async_exclude', $add ); add_filter( 'sgo_javascript_combine_excluded_inline_content', $add ); // Autoptimize (comma-separated string). add_filter( 'autoptimize_filter_js_exclude', function ( $exclude ) { return trim( (string) $exclude . ', cookiebanerpro, gtag(\'consent\'', ', ' ); } ); } /* -------------------------------------------------------------------- * Admin * ------------------------------------------------------------------ */ public function admin_menu() { add_menu_page( 'CookieBanerPro', 'CookieBanerPro', 'manage_options', self::PAGE, array( $this, 'render_page' ), 'dashicons-shield-alt', 81 ); } public function admin_assets( $hook ) { if ( 'toplevel_page_' . self::PAGE !== $hook && 'index.php' !== $hook ) { return; } wp_enqueue_style( 'cookiebanerpro-admin', COOKIEBANERPRO_URL . 'assets/admin.css', array(), COOKIEBANERPRO_VERSION ); } public function action_links( $links ) { $url = admin_url( 'admin.php?page=' . self::PAGE ); array_unshift( $links, '' . esc_html( self::is_connected() ? 'Dashboard' : 'Connect' ) . '' ); return $links; } public static function page_url( $args = array() ) { return add_query_arg( $args, admin_url( 'admin.php?page=' . self::PAGE ) ); } /** After activation, open the plugin page once (skipped for bulk activation and WP-CLI). */ public function activation_redirect() { if ( ! get_transient( 'cookiebanerpro_activated' ) ) { return; } delete_transient( 'cookiebanerpro_activated' ); if ( wp_doing_ajax() || is_network_admin() || isset( $_GET['activate-multi'] ) || ! current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.NonceVerification return; } wp_safe_redirect( self::page_url() ); exit; } public function not_connected_notice() { if ( self::is_connected() || ! current_user_can( 'manage_options' ) ) { return; } // Only on the Dashboard and the Plugins screen, and only until the user dismisses it. $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || ! in_array( $screen->id, array( 'dashboard', 'plugins' ), true ) || get_user_meta( get_current_user_id(), self::META_DISMISSED, true ) ) { return; } printf( '

CookieBanerPro: %s %s | %s

', esc_html__( 'the cookie consent banner is not active yet. Connect your site to turn it on.', 'cookiebanerpro' ), esc_url( self::page_url() ), esc_html__( 'Set up the banner', 'cookiebanerpro' ), esc_url( wp_nonce_url( add_query_arg( 'cookiebanerpro_dismiss', '1' ), 'cookiebanerpro_dismiss' ) ), esc_html__( 'Dismiss', 'cookiebanerpro' ) ); } /** "Dismiss" link of the not-connected notice: hidden for this user for good. */ public function dismiss_notice() { if ( empty( $_GET['cookiebanerpro_dismiss'] ) || ! current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.NonceVerification return; } check_admin_referer( 'cookiebanerpro_dismiss' ); update_user_meta( get_current_user_id(), self::META_DISMISSED, 1 ); wp_safe_redirect( remove_query_arg( array( 'cookiebanerpro_dismiss', '_wpnonce' ) ) ); exit; } /** Address on cookiebanerpro.pl where the user signs up / logs in and approves the connection. */ private function connect_url( $mode ) { // One state for both buttons and for page reloads while it is fresh - a new state per link would // invalidate the one the user actually clicked ("Create a free account" is rendered first). $saved = get_transient( 'cookiebanerpro_state_' . get_current_user_id() ); if ( is_array( $saved ) && isset( $saved['s'], $saved['t'] ) && time() - (int) $saved['t'] < 20 * MINUTE_IN_SECONDS ) { $state = $saved['s']; } else { $state = wp_generate_password( 32, false, false ); set_transient( 'cookiebanerpro_state_' . get_current_user_id(), array( 's' => $state, 't' => time() ), 30 * MINUTE_IN_SECONDS ); } $args = array( 'domain' => rawurlencode( (string) wp_parse_url( home_url(), PHP_URL_HOST ) ), 'return' => rawurlencode( self::page_url() ), 'state' => rawurlencode( $state ), 'mode' => 'signup' === $mode ? 'signup' : 'login', ); return add_query_arg( array_merge( $args, self::theme_args() ), self::service() . '/connect' ); } /** * Theme defaults passed to cookiebanerpro.pl: the partner code (the account is assigned to the theme * author) and the colours/style the new banner starts with. Only well-formed values are sent. */ public static function theme_args() { $theme = self::theme_defaults(); $args = array(); if ( isset( $theme['partner'] ) && preg_match( '/^[A-Za-z0-9]{4,16}$/', (string) $theme['partner'] ) ) { $args['partner'] = (string) $theme['partner']; } if ( isset( $theme['colors'] ) && is_array( $theme['colors'] ) ) { foreach ( array( 'primary', 'background', 'text' ) as $c ) { if ( isset( $theme['colors'][ $c ] ) && preg_match( '/^#[0-9a-fA-F]{6}$/', (string) $theme['colors'][ $c ] ) ) { $args[ 'c_' . $c ] = rawurlencode( (string) $theme['colors'][ $c ] ); } } } if ( isset( $theme['style'] ) && in_array( $theme['style'], array( 'box', 'bar', 'popup' ), true ) ) { $args['style'] = $theme['style']; } if ( ! empty( $theme['dark'] ) ) { $args['dark'] = '1'; } return $args; } /** Handles: return from cookiebanerpro.pl with a one-time code, disconnect, code on/off, refresh. */ public function handle_admin_actions() { if ( ! isset( $_GET['page'] ) || self::PAGE !== $_GET['page'] || ! current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.NonceVerification return; } // 1. Back from cookiebanerpro.pl: exchange the one-time code for the site key and a plugin token. if ( isset( $_GET['cbp_code'], $_GET['state'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $code = sanitize_text_field( wp_unslash( $_GET['cbp_code'] ) ); // phpcs:ignore WordPress.Security.NonceVerification $state = sanitize_text_field( wp_unslash( $_GET['state'] ) ); // phpcs:ignore WordPress.Security.NonceVerification $saved = get_transient( 'cookiebanerpro_state_' . get_current_user_id() ); $expected = is_array( $saved ) && isset( $saved['s'] ) ? (string) $saved['s'] : ''; delete_transient( 'cookiebanerpro_state_' . get_current_user_id() ); if ( '' === $expected || ! hash_equals( $expected, $state ) ) { $this->redirect_with( 'error', 'state' ); } $res = wp_remote_post( self::service() . '/api/wp/exchange', array( 'timeout' => 15, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'code' => $code, 'state' => $state, 'wp_url' => home_url() ) ), ) ); $data = is_wp_error( $res ) ? null : json_decode( wp_remote_retrieve_body( $res ), true ); if ( empty( $data['token'] ) || empty( $data['site_key'] ) ) { $this->redirect_with( 'error', is_wp_error( $res ) ? 'network' : 'exchange' ); } update_option( self::OPT_KEY, sanitize_text_field( $data['site_key'] ) ); update_option( self::OPT_TOKEN, sanitize_text_field( $data['token'] ), false ); update_option( self::OPT_DOMAIN, sanitize_text_field( $data['domain'] ) ); update_option( self::OPT_PANEL, esc_url_raw( $data['panel_url'] ) ); update_option( self::OPT_INJECT, '1' ); update_option( self::OPT_SINCE, time() ); self::forget_status(); $this->redirect_with( 'connected', '1' ); } if ( empty( $_POST['cookiebanerpro_action'] ) ) { return; } check_admin_referer( 'cookiebanerpro_action' ); $action = sanitize_key( wp_unslash( $_POST['cookiebanerpro_action'] ) ); // Basic banner settings from WordPress (colours, style, privacy policy link) - published at once. if ( 'settings' === $action && self::is_connected() ) { $body = array(); foreach ( array( 'primary', 'background', 'text' ) as $c ) { $v = isset( $_POST[ 'cbp_' . $c ] ) ? sanitize_hex_color( wp_unslash( $_POST[ 'cbp_' . $c ] ) ) : ''; if ( $v ) { $body['colors'][ $c ] = $v; } } $style = isset( $_POST['cbp_style'] ) ? sanitize_key( wp_unslash( $_POST['cbp_style'] ) ) : ''; if ( in_array( $style, array( 'box', 'bar', 'popup' ), true ) ) { $body['style'] = $style; } if ( isset( $_POST['cbp_policy'] ) ) { $body['policy_url'] = esc_url_raw( wp_unslash( $_POST['cbp_policy'] ) ); } $res = wp_remote_post( self::service() . '/api/wp/ustawienia', array( 'timeout' => 15, 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . get_option( self::OPT_TOKEN ) ), 'body' => wp_json_encode( $body ), ) ); self::forget_status(); $this->redirect_with( is_wp_error( $res ) || 200 !== (int) wp_remote_retrieve_response_code( $res ) ? 'error' : 'published', is_wp_error( $res ) ? 'network' : '1' ); } if ( 'disconnect' === $action ) { $token = (string) get_option( self::OPT_TOKEN ); if ( $token ) { wp_remote_post( self::service() . '/api/wp/disconnect', array( 'timeout' => 10, 'headers' => array( 'Authorization' => 'Bearer ' . $token ), ) ); } foreach ( array( self::OPT_KEY, self::OPT_TOKEN, self::OPT_DOMAIN, self::OPT_PANEL, self::OPT_SINCE ) as $opt ) { delete_option( $opt ); } self::forget_status(); $this->redirect_with( 'disconnected', '1' ); } if ( 'toggle' === $action ) { update_option( self::OPT_INJECT, '1' === (string) get_option( self::OPT_INJECT, '1' ) ? '0' : '1' ); $this->redirect_with( 'saved', '1' ); } if ( 'toggle_early' === $action ) { update_option( self::OPT_EARLY, '1' === (string) get_option( self::OPT_EARLY, '1' ) ? '0' : '1' ); $this->redirect_with( 'saved', '1' ); } if ( 'refresh' === $action ) { self::forget_status(); $this->redirect_with( 'refreshed', '1' ); } } private function redirect_with( $key, $value ) { wp_safe_redirect( self::page_url( array( $key => $value ) ) ); exit; } /** Site status from cookiebanerpro.pl (plan, page views, published banner), cached for 10 minutes. */ public static function status() { $cached = get_transient( 'cookiebanerpro_status' ); if ( is_array( $cached ) ) { return $cached; } $res = wp_remote_get( self::service() . '/api/wp/status', array( 'timeout' => 10, 'headers' => array( 'Authorization' => 'Bearer ' . get_option( self::OPT_TOKEN ) ), ) ); if ( is_wp_error( $res ) ) { return array( '_error' => 'network' ); } $code = (int) wp_remote_retrieve_response_code( $res ); $data = json_decode( wp_remote_retrieve_body( $res ), true ); if ( 401 === $code ) { return array( '_error' => 'revoked' ); } if ( 200 !== $code || ! is_array( $data ) ) { return array( '_error' => 'server' ); } // The site key can change only on the service side (e.g. site re-created) - keep ours in sync. if ( ! empty( $data['site_key'] ) && preg_match( '/^[a-z0-9]{12}$/', $data['site_key'] ) ) { update_option( self::OPT_KEY, $data['site_key'] ); } set_transient( 'cookiebanerpro_status', $data, self::STATUS_TTL ); return $data; } /** Clears cached status and statistics (after connecting, disconnecting or "Refresh"). */ public static function forget_status() { delete_transient( 'cookiebanerpro_status' ); foreach ( self::RANGES as $days ) { delete_transient( 'cookiebanerpro_stats_' . $days ); } } /** Chosen analytics range (days) from the settings screen. */ public static function range() { $days = isset( $_GET['range'] ) ? (int) $_GET['range'] : 30; // phpcs:ignore WordPress.Security.NonceVerification return in_array( $days, self::RANGES, true ) ? $days : 30; } /** Daily page views and consent decisions of this site for the last N days, cached for 10 minutes. */ public static function stats( $days ) { $days = in_array( (int) $days, self::RANGES, true ) ? (int) $days : 30; $cached = get_transient( 'cookiebanerpro_stats_' . $days ); if ( is_array( $cached ) ) { return $cached; } $res = wp_remote_get( self::service() . '/api/wp/stats?days=' . $days, array( 'timeout' => 10, 'headers' => array( 'Authorization' => 'Bearer ' . get_option( self::OPT_TOKEN ) ), ) ); if ( is_wp_error( $res ) || 200 !== (int) wp_remote_retrieve_response_code( $res ) ) { return null; } $data = json_decode( wp_remote_retrieve_body( $res ), true ); if ( ! is_array( $data ) || empty( $data['series'] ) || ! is_array( $data['series'] ) ) { return null; } set_transient( 'cookiebanerpro_stats_' . $days, $data, self::STATUS_TTL ); return $data; } /** Page views used / plan limit, always as "used / limit" ("used / ∞" on plans without a limit). */ /** * Page views of this site this month. The plan limit counts all sites of the account, so when the account * has more sites, the total is shown below in smaller text. Older service versions send only the total. */ public static function site_usage_html( $status ) { $total = isset( $status['pageviews'] ) ? (int) $status['pageviews'] : 0; if ( ! isset( $status['pageviews_site'] ) || (int) $status['pageviews_site'] === $total ) { return self::usage_html( $status ); } /* translators: %s: page views of all sites in the plan / plan limit */ return esc_html( number_format_i18n( (int) $status['pageviews_site'] ) ) . '' . sprintf( esc_html__( 'All sites in your plan: %s', 'cookiebanerpro' ), self::usage_html( $status ) ) . ''; } public static function usage_html( $status ) { $pv = isset( $status['pageviews'] ) ? (int) $status['pageviews'] : 0; $limit = isset( $status['pageviews_limit'] ) ? (int) $status['pageviews_limit'] : 0; $max = $limit ? number_format_i18n( $limit ) : '∞'; return esc_html( number_format_i18n( $pv ) ) . ' / ' . ( $limit ? esc_html( $max ) : $max ) . ''; } /** Widget in the WordPress dashboard: is the banner working, how much of the plan is left. */ public function dashboard_widget() { if ( ! current_user_can( 'manage_options' ) ) { return; } // Logo instead of the plain name; __widget_basename keeps a text name in "Screen Options". $title = ''; wp_add_dashboard_widget( 'cookiebanerpro_status', $title, array( $this, 'render_dashboard_widget' ), null, array( '__widget_basename' => 'CookieBanerPro' ) ); } public function render_dashboard_widget() { require __DIR__ . '/includes/dashboard-widget.php'; } /** Plan name in English (the service returns Polish names; plan_id is stable). */ public static function plan_label( $status ) { $names = array( 'free' => __( 'Free', 'cookiebanerpro' ), 'start' => 'Start', 'biznes' => __( 'Business', 'cookiebanerpro' ), 'agencja' => __( 'Agency', 'cookiebanerpro' ), 'enterprise' => 'Enterprise', 'admin' => __( 'Administrator', 'cookiebanerpro' ) ); $id = isset( $status['plan_id'] ) ? $status['plan_id'] : ''; return isset( $names[ $id ] ) ? $names[ $id ] : ( isset( $status['plan'] ) ? $status['plan'] : '-' ); } public function render_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } require __DIR__ . '/includes/admin-page.php'; } } register_activation_hook( __FILE__, function () { set_transient( 'cookiebanerpro_activated', 1, 60 ); } ); CookieBanerPro_Plugin::instance();