Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable changes to Warder Cookie Consent are documented here.

## [2.0.1] - 2026-05-28

### Fixed
- `necessary` category `enabled` and `readonly` values were silently overwritten as `false` on every admin settings save. The admin form is submitted via AJAX with `form.serialize()`, which drops `disabled` fields — the readonly checkbox for `necessary` is `disabled`, so it was never submitted. `warder_validate_options` now forces `enabled = true` and `readonly = true` for the `necessary` category regardless of form input. The `enabled` checkbox in the admin UI is also now `disabled` for `necessary`, consistent with `readonly`.
- `is_regex` was always saved as `true` for every cookie. The hidden input used `value=""` for false, so PHP's `isset()` returned `true` for the empty string, silently marking non-regex cookies (`_gid`, `_gat`) as regex patterns and corrupting the autoClear list. The hidden input now outputs `'0'` for false; validation uses `!empty()` with a literal `'0'` check so only the string `'1'` is treated as true.
- Non-necessary categories (e.g. Analytics) were appearing as locked and pre-selected in the frontend preferences modal. `warder_validate_options` was using `isset()` to read `enabled`/`readonly` from DB values rather than enforcing them by policy. PHP now always saves `enabled = false, readonly = false` for non-necessary categories; `src/index.js` derives `enabled`/`readOnly` from the category id rather than trusting DB values; the admin UI replaces the confusing enabled/readonly checkboxes with descriptive lock/unlock icons.
- AJAX save handler no longer returns a misleading "No changes detected" message. `update_option()` returns `false` when the value is unchanged, but the save did succeed — the response now always says "Settings saved successfully."

## [2.0.0] - 2026-05-28

### Changed
Expand Down
2 changes: 1 addition & 1 deletion dist/cookieconsent.bundle.js

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions docs/cookie-category-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Cookie Category Rules

This document defines the hard rules for how cookie categories behave across the admin, PHP layer, and JavaScript frontend. These rules are enforced in code and must not be changed without reviewing all three layers.

---

## Necessary category

| Property | Value | Where enforced |
|---|---|---|
| Enabled | Always `true` | `settings.php` validation, `index.js` config mapping |
| Read-only | Always `true` | `settings.php` validation, `index.js` config mapping |
| User can toggle | No | vanilla-cookieconsent `readOnly: true` |
| Admin UI | Locked — info text only, no checkboxes | `admin.php` |

**Rationale:** Strictly necessary cookies are required for the site to function. They must always be active regardless of user preference.

---

## All other categories (analytics, marketing, custom, …)

| Property | Value | Where enforced |
|---|---|---|
| Enabled | Always `false` | `settings.php` validation, `index.js` config mapping |
| Read-only | Always `false` | `settings.php` validation, `index.js` config mapping |
| User can toggle | Yes | vanilla-cookieconsent `readOnly: false` |
| Admin UI | Info text only, no checkboxes | `admin.php` |

**Rationale:** GDPR requires that non-necessary cookies are opt-in. Pre-selecting or locking optional categories is not permitted. Users must actively choose to enable them.

---

## Where each rule is enforced

### `inc/settings.php` — `warder_validate_options()`

```php
'enabled' => $is_necessary, // true for necessary, false for everything else
'readonly' => $is_necessary, // true for necessary, false for everything else
```

This ensures the database never stores `enabled: true` or `readonly: true` for non-necessary categories, even if a future UI change were to expose those fields again.

### `src/index.js` — `createConfigFromSettings()`

```js
config.categories[categoryId].enabled = (categoryId === 'necessary');
config.categories[categoryId].readOnly = (categoryId === 'necessary');
```

This is the final safety layer. Even if the DB somehow contains stale values, the JavaScript runtime always applies the correct policy before passing the config to vanilla-cookieconsent.

### `inc/admin.php` — category settings row

The "Enabled by default" and "Read-only" checkboxes have been removed. The admin now shows:

- **Necessary:** lock icon + "Always enabled — users cannot turn this off"
- **Other:** unlock icon + "Always off by default — users can opt in"

This eliminates admin-side confusion that previously caused the bug where analytics appeared locked on the frontend.

---

## What to do if a new category is added

1. Add the category via the **Add Category** form in the admin.
2. Give it a title, description, and list the cookies it controls.
3. Do **not** try to make it enabled by default or read-only — the code enforces these as `false` automatically.
4. The category will appear in the preferences modal as an optional, off-by-default toggle.

---

## Browser cookie cache note

vanilla-cookieconsent stores accepted consent in a `cc_cookie` browser cookie. If a visitor previously accepted all cookies while a bug was present (all categories locked/on), their browser still holds the old consent state.

To reset your own browser for testing: open DevTools → Application → Cookies → delete `cc_cookie`, then reload.

New visitors who have never consented will always see the correct default state (necessary on and locked, everything else off and optional).
36 changes: 15 additions & 21 deletions inc/admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -258,27 +258,21 @@ class="regular-text warder-category-title-field"
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Settings', 'warder-cookie-consent' ); ?></th>
<th scope="row"><?php esc_html_e( 'Consent behaviour', 'warder-cookie-consent' ); ?></th>
<td>
<label>
<input type="checkbox" name="warder_options[cookie_categories][<?php echo esc_attr( $category_id ); ?>][enabled]"
<?php checked( $category['enabled'], true ); ?> />
<?php esc_html_e( 'Enabled by default', 'warder-cookie-consent' ); ?>
</label>
<p class="description"><?php esc_html_e( 'If checked, this category will be pre-selected when the user sees the banner.', 'warder-cookie-consent' ); ?></p>
<br>
<label>
<input type="checkbox" name="warder_options[cookie_categories][<?php echo esc_attr( $category_id ); ?>][readonly]"
<?php checked( $category['readonly'], true ); ?>
<?php
if ( 'necessary' === $category_id ) {
echo 'disabled';
}
?>
/>
<?php esc_html_e( 'Read-only (user cannot change)', 'warder-cookie-consent' ); ?>
</label>
<p class="description"><?php esc_html_e( 'If checked, users won\'t be able to toggle this category off. The "necessary" category is always read-only.', 'warder-cookie-consent' ); ?></p>
<?php if ( 'necessary' === $category_id ) : ?>
<p>
<span class="dashicons dashicons-lock" style="color:#d63638;" aria-hidden="true"></span>
<strong><?php esc_html_e( 'Always enabled — users cannot turn this off', 'warder-cookie-consent' ); ?></strong>
</p>
<p class="description"><?php esc_html_e( 'Strictly necessary cookies are required for the website to function. They are always active and locked for all visitors.', 'warder-cookie-consent' ); ?></p>
<?php else : ?>
<p>
<span class="dashicons dashicons-unlock" style="color:#2271b1;" aria-hidden="true"></span>
<strong><?php esc_html_e( 'Always off by default — users can opt in', 'warder-cookie-consent' ); ?></strong>
</p>
<p class="description"><?php esc_html_e( 'Non-necessary cookies are always disabled by default. Visitors must actively choose to enable this category in the cookie preferences panel. This behaviour cannot be changed (GDPR requirement).', 'warder-cookie-consent' ); ?></p>
<?php endif; ?>
</td>
</tr>
</table>
Expand Down Expand Up @@ -306,7 +300,7 @@ class="regular-text warder-category-title-field"
<td>
<input type="hidden"
name="warder_options[cookie_categories][<?php echo esc_attr( $category_id ); ?>][cookies][<?php echo esc_attr( $index ); ?>][is_regex]"
value="<?php echo esc_attr( $cookie['is_regex'] ? '1' : '' ); ?>" />
value="<?php echo esc_attr( $cookie['is_regex'] ? '1' : '0' ); ?>" />
<?php echo $cookie['is_regex'] ? esc_html__( 'Regular Expression', 'warder-cookie-consent' ) : esc_html__( 'Exact Match', 'warder-cookie-consent' ); ?>
</td>
<td>
Expand Down
9 changes: 3 additions & 6 deletions inc/ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,9 @@ function warder_ajax_save_settings() {
$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' ) ) );
}
update_option( 'warder_options', $valid );
delete_transient( 'warder_options_cache' );
wp_send_json_success( array( 'message' => __( 'Settings saved successfully.', 'warder-cookie-consent' ) ) );
}
add_action( 'wp_ajax_warder_save_settings', 'warder_ajax_save_settings' );

Expand Down
10 changes: 7 additions & 3 deletions inc/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,15 @@ function warder_validate_options( $input ) {

$title = isset( $category['title'] ) ? sanitize_text_field( $category['title'] ) : '';

// The 'necessary' category is always enabled and read-only regardless of form input,
// because its checkboxes are disabled in the admin and therefore not submitted.
$is_necessary = ( 'necessary' === $sanitized_id );

$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,
'enabled' => $is_necessary,
'readonly' => $is_necessary,
'cookies' => array(),
);

Expand All @@ -73,7 +77,7 @@ function warder_validate_options( $input ) {
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,
'is_regex' => ! empty( $cookie['is_regex'] ) && '0' !== (string) $cookie['is_regex'],
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "imwz-cookie-consent",
"version": "2.0.0",
"version": "2.0.1",
"main": "webpack.config.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
Expand Down
13 changes: 12 additions & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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: 2.0.0
Stable tag: 2.0.1
Requires PHP: 8.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Expand Down Expand Up @@ -72,6 +72,14 @@ Yes. Settings are versioned via a timestamp that is appended to the script URL,

== Changelog ==

= 2.0.1 =
*2026-05-28*

* Fixed: necessary category enabled/readonly values were silently overwritten as false on every admin settings save because the disabled checkboxes were not submitted by form.serialize(). The necessary category is now always forced to enabled=true and readonly=true in validation, regardless of form input.
* Fixed: is_regex was always saved as true for every cookie. The hidden input used value="" for false, so PHP's isset() returned true for the empty string, silently marking non-regex cookies (_gid, _gat) as regex patterns and corrupting the autoClear cookie list. The hidden input now outputs '0' for false; validation uses !empty() with an explicit '0' check so only the string '1' is treated as true.
* Fixed: non-necessary categories (e.g. Analytics) appeared as locked and pre-selected in the frontend preferences modal. Validation now always saves enabled=false, readonly=false for non-necessary categories; src/index.js derives enabled/readOnly from the category id rather than DB values; admin UI replaces confusing enabled/readonly checkboxes with descriptive lock/unlock icons.
* Fixed: AJAX save no longer returns a misleading "No changes detected" message when a setting is toggled back to its current DB value — the response is always "Settings saved successfully."

= 2.0.0 =
*2026-05-28*

Expand Down Expand Up @@ -180,6 +188,9 @@ Yes. Settings are versioned via a timestamp that is appended to the script URL,

== Upgrade Notice ==

= 2.0.1 =
Fixes three data-corruption bugs: (1) Strictly Necessary category losing its locked state after every save; (2) all cookies being silently saved as regex patterns due to a hidden-input value bug; (3) non-necessary categories appearing locked and pre-selected in the frontend consent modal.

= 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.

Expand Down
8 changes: 5 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,12 @@ function createConfigFromSettings(defaultConfig, wpSettings) {

// Map WordPress categories to the config
Object.entries(settings.cookie_categories).forEach(([categoryId, category]) => {
// Create category if it doesn't exist, or update existing
// Create category if it doesn't exist, or update existing.
// Necessary is always on and locked; all other categories are always
// off by default and optional — users must actively opt in (GDPR).
config.categories[categoryId] = config.categories[categoryId] || {};
config.categories[categoryId].enabled = category.enabled || false;
config.categories[categoryId].readOnly = category.readonly || false;
config.categories[categoryId].enabled = (categoryId === 'necessary');
config.categories[categoryId].readOnly = (categoryId === 'necessary');

// Set up cookie auto-clearing
if (category.cookies && category.cookies.length > 0) {
Expand Down
4 changes: 2 additions & 2 deletions warder-cookie-consent.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/**
* Plugin Name: Warder Cookie Consent
* Description: GDPR-compliant cookie consent banner with category management and floating preferences toggle.
* Version: 2.0.0
* Version: 2.0.1
* Author: Jasper Frumau
* Author URI: https://imagewize.com
* Requires at least: 5.0
Expand All @@ -16,7 +16,7 @@

defined( 'ABSPATH' ) || exit;

define( 'WARDER_VERSION', '2.0.0' );
define( 'WARDER_VERSION', '2.0.1' );
define( 'WARDER_PLUGIN_FILE', __FILE__ );

require_once plugin_dir_path( __FILE__ ) . 'inc/defaults.php';
Expand Down
Loading