diff --git a/CHANGELOG.md b/CHANGELOG.md index 5445266..0749ef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to Warder Cookie Consent are documented here. +## [2.0.0] - 2026-05-28 + +### Changed +- Split plugin logic out of the monolithic `warder-cookie-consent.php` into five focused files under `inc/`: + - `inc/defaults.php` — `warder_get_default_options()`, `warder_get_merged_options()` + - `inc/settings.php` — `warder_register_settings()`, `warder_validate_options()`, `warder_update_options_timestamp()`, `warder_plugin_activate()` + - `inc/ajax.php` — `warder_ajax_save_settings()`, `warder_handle_admin_actions()` + - `inc/admin.php` — `warder_add_options_page()`, `warder_enqueue_admin_scripts()`, `warder_render_options_page()`, `warder_admin_notices()`, `warder_render_category_title_field()` + - `inc/frontend.php` — `warder_enqueue_scripts()`, `warder_get_preferences_toggle_css()`, `warder_add_preferences_button()` +- Main plugin file is now 26 lines: plugin header, `WARDER_VERSION`, `WARDER_PLUGIN_FILE`, and five `require_once` calls +- Added `WARDER_PLUGIN_FILE` constant (`__FILE__` captured at plugin root) so `plugin_dir_url()`, `get_plugin_data()`, and `register_activation_hook()` resolve correctly from within `inc/` files + +No behaviour changes; all existing settings, hooks, and nonces are unaffected. + +### Fixed +- Admin page title changed from "Cookie Consent Settings" to "Warder Cookie Consent" +- Settings sidebar label changed from "Cookie Consent" to "Warder Consent" to avoid ambiguity when other consent plugins are active + ## [1.5.2] - 2026-05-28 ### Added diff --git a/README.md b/README.md index 3884cce..0e23c28 100644 --- a/README.md +++ b/README.md @@ -51,13 +51,13 @@ composer require imagewize/warder-cookie-consent ## Usage -After activation, the cookie consent banner will automatically appear on your website. Configure the settings from the WordPress admin panel at Settings > Cookie Consent. +After activation, the cookie consent banner will automatically appear on your website. Configure the settings from the WordPress admin panel at Settings > Warder Consent. ## Configuration You can customize the plugin through the admin interface: -1. Navigate to Settings > Cookie Consent in your WordPress dashboard +1. Navigate to Settings > Warder Consent in your WordPress dashboard 2. Configure the following settings: - Language and general behavior - Banner title and description @@ -125,7 +125,7 @@ The plugin automatically handles third-party cookies by: 1. Preventing scripts from loading until consent is given 2. Clearing cookies if consent is withdrawn -For detailed implementation guides, see the `dev.md` documentation file. +For detailed implementation guides, see [`docs/dev.md`](docs/dev.md). ## Dependencies diff --git a/dev.md b/dev.md deleted file mode 100644 index 30428f8..0000000 --- a/dev.md +++ /dev/null @@ -1,307 +0,0 @@ -# Simple Cookie Consent - Developer Documentation - -This document provides technical details about how the Simple Cookie Consent plugin works, focusing on how it loads settings from the WordPress admin and blocks cookies until consent is given. - -## Architecture Overview - -The plugin consists of three main components: - -1. **PHP Admin Interface**: Handles settings storage and retrieval in WordPress -2. **JavaScript Configuration**: Configures the vanilla-cookieconsent library -3. **Cookie Consent Banner**: User-facing interface for managing consent - -## How Settings are Loaded from the Options Page - -### Settings Storage - -When a user saves settings in the WordPress admin: - -1. Settings are stored in WordPress options table under the `scc_options` key -2. The plugin validates all inputs using `scc_validate_options()` -3. Default values are provided by `scc_get_default_options()` if needed - -### Settings Transfer to JavaScript - -When a page loads: - -1. `scc_enqueue_scripts()` function loads the bundled JavaScript -2. The same function retrieves settings with `get_option('scc_options')` -3. `wp_localize_script()` passes these settings to JavaScript as `sccSettings.settings` - -```php -wp_localize_script('scc-cookieconsent', 'sccSettings', array( - 'settings' => $options, - 'version' => time() -)); -``` - -### JavaScript Configuration - -In the `src/index.js` file: - -1. Default configuration is defined in the `config` object -2. WordPress settings are loaded from `window.sccSettings` -3. The configuration is updated with user preferences: - -```javascript -// Example of how settings are merged -if (typeof window.sccSettings !== 'undefined') { - const wpSettings = window.sccSettings.settings; - - // Update translations with WordPress settings - if (config.language.translations[config.language.default]) { - config.language.translations[config.language.default].consentModal.title = - wpSettings.title || config.language.translations[config.language.default].consentModal.title; - - // Additional settings mapping... - } -} -``` - -## Cookie Blocking Mechanism - -The plugin uses vanilla-cookieconsent's script blocking capabilities to prevent cookies from being set until consent is given. - -### How Script Blocking Works - -1. Scripts are marked with `data-cookiecategory` attributes to associate them with consent categories -2. The cookie consent library prevents these scripts from executing until the user gives consent for that category -3. The `page_scripts: true` setting enables this functionality - -### Example Script Blocking - -```html - - -``` - -### Cookie Auto-Clearing - -When a user withdraws consent, related cookies can be automatically deleted: - -1. The `autoclear_cookies` setting enables this feature -2. Specific cookies to clear are defined in the configuration: - -```javascript -analytics: { - enabled: false, - readOnly: false, - autoClear: { - cookies: [ - { - name: /^_ga/, // Regular expression to match cookies starting with _ga - }, - { - name: '_gid', // Exact match for _gid cookie - } - ] - } -} -``` - -## Google Analytics Implementation Example - -Here's how to implement Google Analytics with proper consent management: - -1. Add your Google Analytics code to your site with the appropriate data attribute: - -```html - -``` - -2. Ensure the analytics category is configured in the cookie consent setup: - -```javascript -analytics: { - enabled: false, // Disabled by default, requiring explicit consent - readOnly: false, // Can be toggled by the user - autoClear: { - cookies: [ - { name: /^_ga/ }, - { name: '_gid' }, - { name: '_gat' }, - { name: '_ga_.*' } // GA4 cookies - ] - } -} -``` - -## Advanced Usage - -### Adding New Cookie Categories - -To add a new cookie category (e.g., "marketing"): - -1. Add the category to the configuration object in `src/index.js`: - -```javascript -categories: { - // ...existing categories... - marketing: { - enabled: false, - readOnly: false, - autoClear: { - cookies: [ - { name: /^mk_/ }, - { name: 'marketing_session' } - ] - } - } -} -``` - -2. Add the UI elements for this category: - -```javascript -sections: [ - // ...existing sections... - { - title: 'Marketing Cookies', - description: 'These cookies are used to track visitors across websites to display relevant advertisements.', - linkedCategory: 'marketing' - } -] -``` - -3. Mark scripts with the new category: - -```html - -``` - -### Event Handling - -The cookie consent library provides several events you can hook into: - -```javascript -onConsent: ({cookie}) => { - console.log('Consent given:', cookie); - // Perform actions based on consent -}, - -onChange: ({changedCategories, changedServices}) => { - console.log('Consent changed for:', changedCategories); - // React to consent changes -} -``` - -## Managing Cookie Categories and Patterns - -The plugin now allows you to manage cookie categories and specific cookie patterns through the WordPress admin interface. - -### Adding Cookie Categories - -You can add new cookie categories (e.g., marketing, preferences) through the admin interface: - -1. Go to Settings > Cookie Consent -2. At the bottom of the Cookie Categories section, enter a new category ID (e.g., "marketing") -3. Click "Add New Category" -4. Configure the new category's settings (title, description, etc.) -5. Save the settings - -Each category will automatically appear as a new section in the cookie preferences modal. - -### Adding Cookies to Categories - -You can specify which cookies should be blocked until consent is given: - -1. Go to Settings > Cookie Consent -2. Find the relevant cookie category -3. Enter a cookie name or pattern in the input field -4. Check "Regular Expression" if you're using a pattern (e.g., `/^_ga/` will match all cookies starting with "_ga") -5. Click "Add Cookie" - -### How Cookie Patterns Work - -Two types of cookie patterns are supported: - -1. **Exact Match**: Simple cookie names like "_gid" - these will match only the exact cookie name -2. **Regular Expression**: Patterns like "/^_ga/" - these allow matching multiple cookies with similar patterns - -Regular expressions should be entered with the forward slashes (e.g., `/^_ga/`). Some useful patterns: - -- `/^_ga/` - Match all cookies starting with "_ga" -- `/analytics/` - Match all cookies containing "analytics" -- `/^__[a-z]/` - Match all cookies starting with double underscore followed by a lowercase letter - -### How Cookies are Blocked - -When a user has not consented to a particular category: - -1. Scripts marked with `type="text/plain" data-cookiecategory="category_id"` won't execute -2. Cookies matching patterns in that category will be automatically cleared if set - -Example of a script that will only run with analytics consent: - -```html - -``` - -## Troubleshooting - -Common issues and their solutions: - -### Scripts Still Loading Despite No Consent - -Check that: -- The script has the correct `type="text/plain"` attribute -- The `data-cookiecategory` value matches exactly with your category name -- The `page_scripts` setting is enabled - -### Cookies Not Being Cleared - -Check that: -- The `autoclear_cookies` setting is enabled -- Cookie names are correctly specified in the configuration -- The browser supports cookie deletion (some browsers restrict this) - -### Configuration Not Updating - -If your WordPress settings aren't reflected in the cookie banner: -- Check the browser console for JavaScript errors -- Verify that `sccSettings` is properly loaded by adding a debug statement -- Clear browser cache or add a version parameter to prevent caching - -## Using with Tag Managers - -If you're using Google Tag Manager or similar services: - -1. Load the tag manager script with appropriate consent category: - -```html - -``` - -2. Set up consent mode in your tag manager to respect the cookie consent choices diff --git a/docs/dev.md b/docs/dev.md new file mode 100644 index 0000000..33e086b --- /dev/null +++ b/docs/dev.md @@ -0,0 +1,167 @@ +# Warder Cookie Consent — Developer Documentation + +Technical reference for how the plugin loads settings, blocks scripts, and manages cookies. + +## Architecture Overview + +``` +WordPress DB (warder_options key) + → warder_enqueue_scripts() fetches and localizes + → window.warderSettings in browser + → createConfigFromSettings() in src/index.js + → CookieConsent.run(config) +``` + +### File Structure + +``` +warder-cookie-consent/ +├── warder-cookie-consent.php # Plugin header + WARDER_VERSION + WARDER_PLUGIN_FILE + require_once +├── inc/ +│ ├── defaults.php # warder_get_default_options(), warder_get_merged_options() +│ ├── settings.php # warder_register_settings(), warder_validate_options(), +│ │ # warder_update_options_timestamp(), warder_plugin_activate() +│ ├── ajax.php # warder_ajax_save_settings(), warder_handle_admin_actions() +│ ├── admin.php # warder_add_options_page(), warder_enqueue_admin_scripts(), +│ │ # warder_render_options_page(), warder_admin_notices(), +│ │ # warder_render_category_title_field() +│ └── frontend.php # warder_enqueue_scripts(), warder_get_preferences_toggle_css(), +│ # warder_add_preferences_button() +├── assets/js/admin.js # Admin UI: AJAX save, field highlight, add-cookie toggle +├── src/index.js # Frontend bundle entry (webpack input) +└── dist/cookieconsent.bundle.js # Compiled bundle (webpack output) +``` + +## Settings Flow + +### Storage + +Settings are saved to `wp_options` under the key `warder_options`. All input passes through `warder_validate_options()` before writing. + +### Transfer to JavaScript + +`warder_enqueue_scripts()` (in `inc/frontend.php`) passes settings to the browser via `wp_localize_script`: + +```php +wp_localize_script( + 'warder-cookieconsent', + 'warderSettings', + array( + 'settings' => $options, + 'version' => $version, + ) +); +``` + +### JavaScript Configuration + +`createConfigFromSettings()` in `src/index.js` maps the flat `window.warderSettings.settings` object to vanilla-cookieconsent's nested config format: + +```javascript +const wpSettings = window.warderSettings?.settings ?? {}; + +// Banner text +consentModal.title = wpSettings.title; +consentModal.description = wpSettings.description; + +// Categories — built dynamically from wpSettings.cookie_categories +``` + +## Script Blocking + +Mark any script tag with `type="text/plain"` and `data-category` to gate it behind consent: + +```html + + +``` + +The `page_scripts: true` setting (enabled by default) activates this behaviour in vanilla-cookieconsent. + +## Cookie Auto-Clearing + +When consent is withdrawn, cookies matching patterns in that category are deleted automatically. Patterns are configured per-category in the admin UI (Settings > Warder Consent) and stored as: + +```php +'cookies' => [ + [ 'name' => '/^_ga/', 'is_regex' => true ], + [ 'name' => '_gid', 'is_regex' => false ], +] +``` + +`is_regex` entries are converted to JavaScript `RegExp` objects in `createConfigFromSettings()`: + +```javascript +if ( cookie.is_regex ) { + const match = cookie.name.match( /^\/(.+)\/([gimsuy]*)$/ ); + name = match ? new RegExp( match[1], match[2] ) : cookie.name; +} +``` + +## Managing Cookie Categories + +All category management is done through the admin UI at **Settings > Warder Consent**. No code changes needed. + +- **Add a category** — enter a slug (e.g. `marketing`) in "Add New Category" +- **Add a cookie pattern** — click "Add Cookie to this Category" within any category +- **Delete a cookie or category** — use the Remove / Delete Category links + +## Google Analytics Example + +```html + + +``` + +Pre-configured analytics cookie patterns cover Google Analytics (`/^_ga/`, `_gid`, `_gat`) and Matomo (`/^_pk_/`, `/^mtm_/`) out of the box. + +## Event Hooks + +vanilla-cookieconsent fires callbacks you can use in `src/index.js`: + +```javascript +CookieConsent.run({ + // ...config, + onConsent: ({ cookie }) => { + if ( CookieConsent.acceptedCategory('analytics') ) { + // analytics accepted + } + }, + onChange: ({ changedCategories }) => { + // react to consent changes + }, +}); +``` + +## Admin AJAX Save + +Settings save without a page reload via `wp_ajax_warder_save_settings`. The handler lives in `inc/ajax.php`; the JS intercept in `assets/js/admin.js`. The nonce used is `warder_options_group-options` (generated by `settings_fields()`). + +## Cache Busting + +`warder_update_options_timestamp()` writes `time()` to `warder_options_last_updated` on every option save. `warder_enqueue_scripts()` uses this value as the script version, so browser caches are invalidated automatically when settings change. + +## Troubleshooting + +**Scripts still loading despite no consent** +- Confirm `type="text/plain"` is present on the script tag +- Confirm `data-category` matches the category slug exactly (e.g. `analytics`, not `Analytics`) +- Confirm "Page Scripts" is enabled in Settings > Warder Consent + +**Cookies not clearing** +- Confirm "Auto-clear Cookies" is enabled +- Confirm the cookie pattern is correct — test regex patterns at regex101.com +- Cookie deletion requires the cookie to share the same domain and path + +**Settings not reflected in the banner** +- Open the browser console and inspect `window.warderSettings.settings` +- Hard-refresh (Cmd+Shift+R) to bypass the browser cache diff --git a/inc/admin.php b/inc/admin.php new file mode 100644 index 0000000..7596a68 --- /dev/null +++ b/inc/admin.php @@ -0,0 +1,484 @@ + admin_url( 'admin-ajax.php' ), + 'save' => __( 'Save Settings', 'warder-cookie-consent' ), + 'saving' => __( 'Saving…', 'warder-cookie-consent' ), + ) + ); +} +add_action( 'admin_enqueue_scripts', 'warder_enqueue_admin_scripts' ); + +/** + * Renders the plugin settings page in the WordPress admin. + */ +function warder_render_options_page() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + // Nonce already verified by options.php before the redirect that sets this param. + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $settings_updated = isset( $_GET['settings-updated'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['settings-updated'] ) ); + if ( $settings_updated ) { + delete_transient( 'warder_options_cache' ); + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $warder_notice = isset( $_GET['warder_notice'] ) ? sanitize_key( wp_unslash( $_GET['warder_notice'] ) ) : ''; + + $options = get_option( 'warder_options', array() ); + $default_options = warder_get_default_options(); + $options = wp_parse_args( $options, $default_options ); + + if ( ! isset( $options['cookie_categories'] ) || ! is_array( $options['cookie_categories'] ) ) { + $options['cookie_categories'] = $default_options['cookie_categories']; + } + + $options = warder_handle_admin_actions( $options ); + + ?> +
+
' . sprintf( + /* translators: 1: Plugin name, 2: HTML link to settings page. */ + esc_html__( 'Thank you for installing %1$s! Please configure your settings on the %2$s.', 'warder-cookie-consent' ), + esc_html( $plugin_name ), + '' . esc_html__( 'settings page', 'warder-cookie-consent' ) . '' + ) . '
'; + echo ''; +} +add_action( 'admin_notices', 'warder_admin_notices' ); + +/** + * Renders an input field for a cookie category title with the correct CSS class. + * + * @param string $category_id The category identifier. + * @param string $title The current category title. + */ +function warder_render_category_title_field( $category_id, $title ) { + ?> + + + __( 'Unauthorized.', 'warder-cookie-consent' ) ) ); + } + + $input = isset( $_POST['warder_options'] ) ? wp_unslash( $_POST['warder_options'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $valid = warder_validate_options( $input ); + + if ( update_option( 'warder_options', $valid ) ) { + delete_transient( 'warder_options_cache' ); + wp_send_json_success( array( 'message' => __( 'Settings saved successfully.', 'warder-cookie-consent' ) ) ); + } else { + wp_send_json_success( array( 'message' => __( 'No changes detected.', 'warder-cookie-consent' ) ) ); + } +} +add_action( 'wp_ajax_warder_save_settings', 'warder_ajax_save_settings' ); + +/** + * Processes add/delete actions for cookie categories and cookies on the settings page. + * + * @param array $options The current merged plugin options. + * @return array The options after any add/delete action has been applied. + */ +function warder_handle_admin_actions( $options ) { + if ( ! current_user_can( 'manage_options' ) ) { + return $options; + } + + $changed = false; + + // Add a new cookie category. + if ( isset( $_POST['warder_add_category'], $_POST['warder_category_nonce'] ) ) { + check_admin_referer( 'warder_add_category', 'warder_category_nonce' ); + + $new_id = isset( $_POST['new_category_id'] ) ? sanitize_key( wp_unslash( $_POST['new_category_id'] ) ) : ''; + + if ( '' !== $new_id && ! isset( $options['cookie_categories'][ $new_id ] ) ) { + $options['cookie_categories'][ $new_id ] = array( + 'title' => ucfirst( $new_id ), + 'description' => '', + 'enabled' => false, + 'readonly' => false, + 'cookies' => array(), + ); + $changed = true; + } + } + + // Add a cookie to an existing category. + if ( isset( $_POST['warder_add_cookie'], $_POST['warder_cookie_nonce'] ) ) { + check_admin_referer( 'warder_add_cookie', 'warder_cookie_nonce' ); + + $category_id = isset( $_POST['category_id'] ) ? sanitize_key( wp_unslash( $_POST['category_id'] ) ) : ''; + $cookie_name = isset( $_POST['cookie_name'] ) ? sanitize_text_field( wp_unslash( $_POST['cookie_name'] ) ) : ''; + $is_regex = isset( $_POST['is_regex'] ); + + if ( '' !== $cookie_name && isset( $options['cookie_categories'][ $category_id ] ) ) { + $options['cookie_categories'][ $category_id ]['cookies'][] = array( + 'name' => $cookie_name, + 'is_regex' => $is_regex, + ); + $changed = true; + } + } + + // Delete a cookie category. + if ( isset( $_GET['action'] ) && 'delete_category' === sanitize_key( wp_unslash( $_GET['action'] ) ) ) { + $category_id = isset( $_GET['category'] ) ? sanitize_key( wp_unslash( $_GET['category'] ) ) : ''; + $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; + + if ( wp_verify_nonce( $nonce, 'delete_category_' . $category_id ) && 'necessary' !== $category_id && isset( $options['cookie_categories'][ $category_id ] ) ) { + unset( $options['cookie_categories'][ $category_id ] ); + $changed = true; + } + } + + // Delete a single cookie from a category. + if ( isset( $_GET['action'] ) && 'delete_cookie' === sanitize_key( wp_unslash( $_GET['action'] ) ) ) { + $category_id = isset( $_GET['category'] ) ? sanitize_key( wp_unslash( $_GET['category'] ) ) : ''; + $cookie_index = isset( $_GET['cookie_index'] ) ? absint( wp_unslash( $_GET['cookie_index'] ) ) : -1; + $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; + + if ( $cookie_index >= 0 && wp_verify_nonce( $nonce, 'delete_cookie_' . $category_id . '_' . $cookie_index ) && isset( $options['cookie_categories'][ $category_id ]['cookies'][ $cookie_index ] ) ) { + array_splice( $options['cookie_categories'][ $category_id ]['cookies'], $cookie_index, 1 ); + $changed = true; + } + } + + if ( $changed ) { + update_option( 'warder_options', $options ); + delete_transient( 'warder_options_cache' ); + wp_safe_redirect( + add_query_arg( + array( + 'page' => 'warder-cookie-consent', + 'warder_notice' => 'saved', + ), + admin_url( 'options-general.php' ) + ) + ); + exit; + } + + return $options; +} diff --git a/inc/defaults.php b/inc/defaults.php new file mode 100644 index 0000000..284ec71 --- /dev/null +++ b/inc/defaults.php @@ -0,0 +1,85 @@ + true, + 'current_lang' => 'en', + 'autoclear_cookies' => true, + 'page_scripts' => true, + 'title' => 'We use cookies!', + 'description' => 'Hello, this website uses essential cookies to ensure its proper operation and tracking cookies to understand how you interact with it. The latter will be set only after consent.', + 'primary_btn_text' => 'Accept all', + 'primary_btn_role' => 'accept_all', + 'secondary_btn_text' => 'Reject all', + 'secondary_btn_role' => 'accept_necessary', + 'privacy_policy_url' => '#privacy-policy', + 'show_preferences_toggle' => true, + 'preferences_toggle_position' => 'bottom-right', + 'cookie_categories' => array( + 'necessary' => array( + 'title' => 'Strictly Necessary', + 'description' => 'These cookies are essential for the proper functioning of the website and cannot be disabled.', + 'enabled' => true, + 'readonly' => true, + 'cookies' => array( + array( + 'name' => '/^sbjs_/', + 'is_regex' => true, + ), + ), + ), + 'analytics' => array( + 'title' => 'Performance and Analytics', + 'description' => 'These cookies collect information about how you use our website. All of the data is anonymized and cannot be used to identify you.', + 'enabled' => false, + 'readonly' => false, + 'cookies' => array( + array( + 'name' => '/^_ga/', + 'is_regex' => true, + ), + array( + 'name' => '_gid', + 'is_regex' => false, + ), + array( + 'name' => '_gat', + 'is_regex' => false, + ), + array( + 'name' => '/^_pk_/', + 'is_regex' => true, + ), + array( + 'name' => '/^mtm_/', + 'is_regex' => true, + ), + ), + ), + ), + ); +} + +/** + * Retrieves options from the database and deep-merges with defaults. + * + * @return array + */ +function warder_get_merged_options() { + $options = get_option( 'warder_options', array() ); + $default_options = warder_get_default_options(); + + return wp_parse_args( $options, $default_options ); +} diff --git a/inc/frontend.php b/inc/frontend.php new file mode 100644 index 0000000..0ad6559 --- /dev/null +++ b/inc/frontend.php @@ -0,0 +1,112 @@ + 'defer', + 'in_footer' => true, + ) + ); + + wp_localize_script( + 'warder-cookieconsent', + 'warderSettings', + array( + 'settings' => $options, + 'version' => $version, + ) + ); + + if ( ! empty( $options['show_preferences_toggle'] ) ) { + wp_register_style( 'warder-preferences-toggle', false, array(), $version ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion + wp_enqueue_style( 'warder-preferences-toggle' ); + wp_add_inline_style( 'warder-preferences-toggle', wp_strip_all_tags( warder_get_preferences_toggle_css() ) ); + } +} +add_action( 'wp_enqueue_scripts', 'warder_enqueue_scripts' ); + +/** + * Returns CSS for the floating preferences toggle button. + * + * @return string + */ +function warder_get_preferences_toggle_css() { + return ' +.warder-preferences-toggle { + position: fixed; + width: 48px; + height: 48px; + border-radius: 50%; + background: #333; + color: #fff; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); + z-index: 9999; + transition: background 0.2s ease, transform 0.2s ease; +} +.warder-preferences-toggle:hover { + background: #555; + transform: scale(1.1); +} +.warder-preferences-toggle svg { + width: 24px; + height: 24px; + pointer-events: none; +} +.warder-preferences-toggle--bottom-right { bottom: 20px; right: 20px; } +.warder-preferences-toggle--bottom-left { bottom: 20px; left: 20px; } +.warder-preferences-toggle--top-right { top: 20px; right: 20px; } +.warder-preferences-toggle--top-left { top: 20px; left: 20px; } +'; +} + +/** + * Outputs the floating preferences toggle button in the footer. + */ +function warder_add_preferences_button() { + $options = warder_get_merged_options(); + if ( empty( $options['enabled'] ) || empty( $options['show_preferences_toggle'] ) ) { + return; + } + + $allowed = array( 'bottom-right', 'bottom-left', 'top-right', 'top-left' ); + $position = isset( $options['preferences_toggle_position'] ) && in_array( $options['preferences_toggle_position'], $allowed, true ) + ? $options['preferences_toggle_position'] + : 'bottom-right'; + + echo ''; +} +add_action( 'wp_footer', 'warder_add_preferences_button' ); diff --git a/inc/settings.php b/inc/settings.php new file mode 100644 index 0000000..2ba0db0 --- /dev/null +++ b/inc/settings.php @@ -0,0 +1,109 @@ + 'array', + 'sanitize_callback' => 'warder_validate_options', + ) + ); + + if ( false === get_option( 'warder_options' ) ) { + add_option( 'warder_options', warder_get_default_options() ); + } +} +add_action( 'admin_init', 'warder_register_settings' ); + +/** + * Sanitizes and validates options before saving to the database. + * + * @param array $input Raw input from the settings form. + * @return array Sanitized options. + */ +function warder_validate_options( $input ) { + $valid = array(); + + $valid['enabled'] = isset( $input['enabled'] ) ? true : false; + $valid['current_lang'] = sanitize_text_field( $input['current_lang'] ); + $valid['autoclear_cookies'] = isset( $input['autoclear_cookies'] ) ? true : false; + $valid['page_scripts'] = isset( $input['page_scripts'] ) ? true : false; + $valid['title'] = sanitize_text_field( $input['title'] ); + $valid['description'] = wp_kses_post( $input['description'] ); + $valid['primary_btn_text'] = sanitize_text_field( $input['primary_btn_text'] ); + $valid['primary_btn_role'] = in_array( $input['primary_btn_role'], array( 'accept_all', 'accept_selected' ), true ) + ? $input['primary_btn_role'] : 'accept_all'; + $valid['secondary_btn_text'] = sanitize_text_field( $input['secondary_btn_text'] ); + $valid['secondary_btn_role'] = in_array( $input['secondary_btn_role'], array( 'accept_necessary', 'settings' ), true ) + ? $input['secondary_btn_role'] : 'accept_necessary'; + $valid['privacy_policy_url'] = esc_url_raw( $input['privacy_policy_url'] ); + $valid['show_preferences_toggle'] = isset( $input['show_preferences_toggle'] ) ? true : false; + $valid['preferences_toggle_position'] = in_array( $input['preferences_toggle_position'], array( 'bottom-right', 'bottom-left', 'top-right', 'top-left' ), true ) + ? $input['preferences_toggle_position'] : 'bottom-right'; + + if ( isset( $input['cookie_categories'] ) && is_array( $input['cookie_categories'] ) ) { + $valid['cookie_categories'] = array(); + + foreach ( $input['cookie_categories'] as $category_id => $category ) { + $sanitized_id = sanitize_key( $category_id ); + + $title = isset( $category['title'] ) ? sanitize_text_field( $category['title'] ) : ''; + + $valid['cookie_categories'][ $sanitized_id ] = array( + 'title' => $title, + 'description' => wp_kses_post( $category['description'] ), + 'enabled' => isset( $category['enabled'] ) ? true : false, + 'readonly' => isset( $category['readonly'] ) ? true : false, + 'cookies' => array(), + ); + + if ( isset( $category['cookies'] ) && is_array( $category['cookies'] ) ) { + foreach ( $category['cookies'] as $cookie ) { + if ( ! empty( $cookie['name'] ) ) { + $valid['cookie_categories'][ $sanitized_id ]['cookies'][] = array( + 'name' => sanitize_text_field( $cookie['name'] ), + 'is_regex' => isset( $cookie['is_regex'] ) ? true : false, + ); + } + } + } + } + } + + return $valid; +} + +add_action( 'update_option_warder_options', 'warder_update_options_timestamp', 10, 2 ); +/** + * Updates the options timestamp whenever the plugin settings are saved. + * + * @param mixed $old_value Previous option value (unused). + * @param mixed $new_value New option value (unused). + */ +function warder_update_options_timestamp( $old_value, $new_value ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed + update_option( 'warder_options_last_updated', time() ); +} + +register_activation_hook( WARDER_PLUGIN_FILE, 'warder_plugin_activate' ); +/** + * Merges existing options with defaults on plugin activation to preserve user data. + */ +function warder_plugin_activate() { + $options = get_option( 'warder_options', array() ); + $default_options = warder_get_default_options(); + + $merged_options = wp_parse_args( $options, $default_options ); + + update_option( 'warder_options', $merged_options ); +} diff --git a/package.json b/package.json index f44c03f..9c35475 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "imwz-cookie-consent", - "version": "1.5.2", + "version": "2.0.0", "main": "webpack.config.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/readme.txt b/readme.txt index 6d80f04..04dceb4 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Donate link: https://imagewize.com Tags: cookie, consent, gdpr, privacy, compliance Requires at least: 5.0 Tested up to: 7.0 -Stable tag: 1.5.2 +Stable tag: 2.0.0 Requires PHP: 8.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -72,6 +72,12 @@ Yes. Settings are versioned via a timestamp that is appended to the script URL, == Changelog == += 2.0.0 = +*2026-05-28* + +* Changed: plugin logic split from one monolithic file into five focused files under `inc/` — `defaults.php`, `settings.php`, `ajax.php`, `admin.php`, `frontend.php`. Main plugin file is now 26 lines (header, constants, requires). No behaviour changes. +* Fixed: admin page title renamed to "Warder Cookie Consent"; Settings sidebar label renamed to "Warder Consent" + = 1.5.2 = *2026-05-28* @@ -174,6 +180,9 @@ Yes. Settings are versioned via a timestamp that is appended to the script URL, == Upgrade Notice == += 2.0.0 = +Internal refactor only — plugin logic split into `inc/` files for maintainability. Admin page title and Settings sidebar label updated to "Warder Cookie Consent" / "Warder Consent". No settings migration required, no behaviour changes. + = 1.5.2 = Save All Settings now uses AJAX, so the page no longer jumps back to the top after saving. Add Cookie forms are no longer nested inside the main settings form, so the regex checkbox and the rest of the cookie inputs submit reliably. diff --git a/warder-cookie-consent.php b/warder-cookie-consent.php index 147859c..e8f009f 100644 --- a/warder-cookie-consent.php +++ b/warder-cookie-consent.php @@ -2,7 +2,7 @@ /** * Plugin Name: Warder Cookie Consent * Description: GDPR-compliant cookie consent banner with category management and floating preferences toggle. - * Version: 1.5.2 + * Version: 2.0.0 * Author: Jasper Frumau * Author URI: https://imagewize.com * Requires at least: 5.0 @@ -16,873 +16,11 @@ defined( 'ABSPATH' ) || exit; -define( 'WARDER_VERSION', '1.5.2' ); +define( 'WARDER_VERSION', '2.0.0' ); +define( 'WARDER_PLUGIN_FILE', __FILE__ ); -/** - * Registers plugin settings and adds default options on first activation. - */ -function warder_register_settings() { - register_setting( - 'warder_options_group', - 'warder_options', - array( - 'type' => 'array', - 'sanitize_callback' => 'warder_validate_options', - ) - ); - - if ( false === get_option( 'warder_options' ) ) { - add_option( 'warder_options', warder_get_default_options() ); - } -} -add_action( 'admin_init', 'warder_register_settings' ); - -/** - * Returns the canonical default options structure. - * - * @return array - */ -function warder_get_default_options() { - return array( - 'enabled' => true, - 'current_lang' => 'en', - 'autoclear_cookies' => true, - 'page_scripts' => true, - 'title' => 'We use cookies!', - 'description' => 'Hello, this website uses essential cookies to ensure its proper operation and tracking cookies to understand how you interact with it. The latter will be set only after consent.', - 'primary_btn_text' => 'Accept all', - 'primary_btn_role' => 'accept_all', - 'secondary_btn_text' => 'Reject all', - 'secondary_btn_role' => 'accept_necessary', - 'privacy_policy_url' => '#privacy-policy', - 'show_preferences_toggle' => true, - 'preferences_toggle_position' => 'bottom-right', - 'cookie_categories' => array( - 'necessary' => array( - 'title' => 'Strictly Necessary', - 'description' => 'These cookies are essential for the proper functioning of the website and cannot be disabled.', - 'enabled' => true, - 'readonly' => true, - 'cookies' => array( - array( - 'name' => '/^sbjs_/', - 'is_regex' => true, - ), - ), - ), - 'analytics' => array( - 'title' => 'Performance and Analytics', - 'description' => 'These cookies collect information about how you use our website. All of the data is anonymized and cannot be used to identify you.', - 'enabled' => false, - 'readonly' => false, - 'cookies' => array( - array( - 'name' => '/^_ga/', - 'is_regex' => true, - ), - array( - 'name' => '_gid', - 'is_regex' => false, - ), - array( - 'name' => '_gat', - 'is_regex' => false, - ), - array( - 'name' => '/^_pk_/', - 'is_regex' => true, - ), - array( - 'name' => '/^mtm_/', - 'is_regex' => true, - ), - ), - ), - ), - ); -} - -/** - * Sanitizes and validates options before saving to the database. - * - * @param array $input Raw input from the settings form. - * @return array Sanitized options. - */ -function warder_validate_options( $input ) { - $valid = array(); - - $valid['enabled'] = isset( $input['enabled'] ) ? true : false; - $valid['current_lang'] = sanitize_text_field( $input['current_lang'] ); - $valid['autoclear_cookies'] = isset( $input['autoclear_cookies'] ) ? true : false; - $valid['page_scripts'] = isset( $input['page_scripts'] ) ? true : false; - $valid['title'] = sanitize_text_field( $input['title'] ); - $valid['description'] = wp_kses_post( $input['description'] ); - $valid['primary_btn_text'] = sanitize_text_field( $input['primary_btn_text'] ); - $valid['primary_btn_role'] = in_array( $input['primary_btn_role'], array( 'accept_all', 'accept_selected' ), true ) - ? $input['primary_btn_role'] : 'accept_all'; - $valid['secondary_btn_text'] = sanitize_text_field( $input['secondary_btn_text'] ); - $valid['secondary_btn_role'] = in_array( $input['secondary_btn_role'], array( 'accept_necessary', 'settings' ), true ) - ? $input['secondary_btn_role'] : 'accept_necessary'; - $valid['privacy_policy_url'] = esc_url_raw( $input['privacy_policy_url'] ); - $valid['show_preferences_toggle'] = isset( $input['show_preferences_toggle'] ) ? true : false; - $valid['preferences_toggle_position'] = in_array( $input['preferences_toggle_position'], array( 'bottom-right', 'bottom-left', 'top-right', 'top-left' ), true ) - ? $input['preferences_toggle_position'] : 'bottom-right'; - - if ( isset( $input['cookie_categories'] ) && is_array( $input['cookie_categories'] ) ) { - $valid['cookie_categories'] = array(); - - foreach ( $input['cookie_categories'] as $category_id => $category ) { - $sanitized_id = sanitize_key( $category_id ); - - $title = isset( $category['title'] ) ? sanitize_text_field( $category['title'] ) : ''; - - $valid['cookie_categories'][ $sanitized_id ] = array( - 'title' => $title, - 'description' => wp_kses_post( $category['description'] ), - 'enabled' => isset( $category['enabled'] ) ? true : false, - 'readonly' => isset( $category['readonly'] ) ? true : false, - 'cookies' => array(), - ); - - if ( isset( $category['cookies'] ) && is_array( $category['cookies'] ) ) { - foreach ( $category['cookies'] as $cookie ) { - if ( ! empty( $cookie['name'] ) ) { - $valid['cookie_categories'][ $sanitized_id ]['cookies'][] = array( - 'name' => sanitize_text_field( $cookie['name'] ), - 'is_regex' => isset( $cookie['is_regex'] ) ? true : false, - ); - } - } - } - } - } - - return $valid; -} - -/** - * Registers the plugin settings page under the Settings menu. - */ -function warder_add_options_page() { - add_options_page( - 'Cookie Consent Settings', - 'Cookie Consent', - 'manage_options', - 'warder-cookie-consent', - 'warder_render_options_page' - ); -} -add_action( 'admin_menu', 'warder_add_options_page' ); - -/** - * Enqueues jQuery-dependent admin scripts for the plugin settings page. - * - * @param string $hook The current admin page hook suffix. - */ -function warder_enqueue_admin_scripts( $hook ) { - if ( 'settings_page_warder-cookie-consent' !== $hook ) { - return; - } - - wp_enqueue_script( - 'warder-admin', - plugin_dir_url( __FILE__ ) . 'assets/js/admin.js', - array( 'jquery' ), - WARDER_VERSION, - true - ); - - wp_localize_script( - 'warder-admin', - 'warderAdmin', - array( - 'ajaxurl' => admin_url( 'admin-ajax.php' ), - 'save' => __( 'Save Settings', 'warder-cookie-consent' ), - 'saving' => __( 'Saving…', 'warder-cookie-consent' ), - ) - ); -} -add_action( 'admin_enqueue_scripts', 'warder_enqueue_admin_scripts' ); - -/** - * Handles AJAX save of plugin settings from the admin settings page. - */ -function warder_ajax_save_settings() { - check_ajax_referer( 'warder_options_group-options', '_wpnonce' ); - - if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'warder-cookie-consent' ) ) ); - } - - $input = isset( $_POST['warder_options'] ) ? wp_unslash( $_POST['warder_options'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - $valid = warder_validate_options( $input ); - - if ( update_option( 'warder_options', $valid ) ) { - delete_transient( 'warder_options_cache' ); - wp_send_json_success( array( 'message' => __( 'Settings saved successfully.', 'warder-cookie-consent' ) ) ); - } else { - wp_send_json_success( array( 'message' => __( 'No changes detected.', 'warder-cookie-consent' ) ) ); - } -} -add_action( 'wp_ajax_warder_save_settings', 'warder_ajax_save_settings' ); - -/** - * Processes add/delete actions for cookie categories and cookies on the settings page. - * - * @param array $options The current merged plugin options. - * @return array The options after any add/delete action has been applied. - */ -function warder_handle_admin_actions( $options ) { - if ( ! current_user_can( 'manage_options' ) ) { - return $options; - } - - $changed = false; - - // Add a new cookie category. - if ( isset( $_POST['warder_add_category'], $_POST['warder_category_nonce'] ) ) { - check_admin_referer( 'warder_add_category', 'warder_category_nonce' ); - - $new_id = isset( $_POST['new_category_id'] ) ? sanitize_key( wp_unslash( $_POST['new_category_id'] ) ) : ''; - - if ( '' !== $new_id && ! isset( $options['cookie_categories'][ $new_id ] ) ) { - $options['cookie_categories'][ $new_id ] = array( - 'title' => ucfirst( $new_id ), - 'description' => '', - 'enabled' => false, - 'readonly' => false, - 'cookies' => array(), - ); - $changed = true; - } - } - - // Add a cookie to an existing category. - if ( isset( $_POST['warder_add_cookie'], $_POST['warder_cookie_nonce'] ) ) { - check_admin_referer( 'warder_add_cookie', 'warder_cookie_nonce' ); - - $category_id = isset( $_POST['category_id'] ) ? sanitize_key( wp_unslash( $_POST['category_id'] ) ) : ''; - $cookie_name = isset( $_POST['cookie_name'] ) ? sanitize_text_field( wp_unslash( $_POST['cookie_name'] ) ) : ''; - $is_regex = isset( $_POST['is_regex'] ); - - if ( '' !== $cookie_name && isset( $options['cookie_categories'][ $category_id ] ) ) { - $options['cookie_categories'][ $category_id ]['cookies'][] = array( - 'name' => $cookie_name, - 'is_regex' => $is_regex, - ); - $changed = true; - } - } - - // Delete a cookie category. - if ( isset( $_GET['action'] ) && 'delete_category' === sanitize_key( wp_unslash( $_GET['action'] ) ) ) { - $category_id = isset( $_GET['category'] ) ? sanitize_key( wp_unslash( $_GET['category'] ) ) : ''; - $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; - - if ( wp_verify_nonce( $nonce, 'delete_category_' . $category_id ) && 'necessary' !== $category_id && isset( $options['cookie_categories'][ $category_id ] ) ) { - unset( $options['cookie_categories'][ $category_id ] ); - $changed = true; - } - } - - // Delete a single cookie from a category. - if ( isset( $_GET['action'] ) && 'delete_cookie' === sanitize_key( wp_unslash( $_GET['action'] ) ) ) { - $category_id = isset( $_GET['category'] ) ? sanitize_key( wp_unslash( $_GET['category'] ) ) : ''; - $cookie_index = isset( $_GET['cookie_index'] ) ? absint( wp_unslash( $_GET['cookie_index'] ) ) : -1; - $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; - - if ( $cookie_index >= 0 && wp_verify_nonce( $nonce, 'delete_cookie_' . $category_id . '_' . $cookie_index ) && isset( $options['cookie_categories'][ $category_id ]['cookies'][ $cookie_index ] ) ) { - array_splice( $options['cookie_categories'][ $category_id ]['cookies'], $cookie_index, 1 ); - $changed = true; - } - } - - if ( $changed ) { - update_option( 'warder_options', $options ); - delete_transient( 'warder_options_cache' ); - wp_safe_redirect( - add_query_arg( - array( - 'page' => 'warder-cookie-consent', - 'warder_notice' => 'saved', - ), - admin_url( 'options-general.php' ) - ) - ); - exit; - } - - return $options; -} - -/** - * Renders the plugin settings page in the WordPress admin. - */ -function warder_render_options_page() { - if ( ! current_user_can( 'manage_options' ) ) { - return; - } - - // Nonce already verified by options.php before the redirect that sets this param. - // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $settings_updated = isset( $_GET['settings-updated'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['settings-updated'] ) ); - if ( $settings_updated ) { - delete_transient( 'warder_options_cache' ); - } - - // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $warder_notice = isset( $_GET['warder_notice'] ) ? sanitize_key( wp_unslash( $_GET['warder_notice'] ) ) : ''; - - $options = get_option( 'warder_options', array() ); - $default_options = warder_get_default_options(); - $options = wp_parse_args( $options, $default_options ); - - if ( ! isset( $options['cookie_categories'] ) || ! is_array( $options['cookie_categories'] ) ) { - $options['cookie_categories'] = $default_options['cookie_categories']; - } - - $options = warder_handle_admin_actions( $options ); - - ?> --
' . sprintf( - /* translators: 1: Plugin name, 2: HTML link to settings page. */ - esc_html__( 'Thank you for installing %1$s! Please configure your settings on the %2$s.', 'warder-cookie-consent' ), - esc_html( $plugin_name ), - '' . esc_html__( 'settings page', 'warder-cookie-consent' ) . '' - ) . '
'; - echo '