From cc9bb26618553a9e544201d35a1876674a064737 Mon Sep 17 00:00:00 2001 From: Jasper Frumau Date: Thu, 28 May 2026 10:50:53 +0700 Subject: [PATCH 01/10] docs: align CLAUDE.md and architecture plan with warder_ codebase Replace outdated scc_ prefix references with warder_ throughout CLAUDE.md, update the PHP layer section to reflect the actual file and function names, expand the settings structure, and correct the Composer package name. Rewrite docs/architecture-plan.md to accurately describe current state, adopt AJAX save as the single recommended approach for Phase 1, add a Phase 2 for extracting inline admin JS to a file, and incorporate relevant WordPress.org plugin guideline references (Guidelines 4, 7, 8, 11, 13). --- CLAUDE.md | 48 +++++--- docs/architecture-plan.md | 245 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 275 insertions(+), 18 deletions(-) create mode 100644 docs/architecture-plan.md diff --git a/CLAUDE.md b/CLAUDE.md index c5db96d..ff4d467 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,7 @@ There are no tests. PHP requires 8.0+. To install as a Composer dependency: ```bash -composer require imagewize/simple-cookie-consent +composer require imagewize/warder-cookie-consent ``` ## Architecture @@ -26,23 +26,23 @@ composer require imagewize/simple-cookie-consent ### Data Flow ``` -WordPress DB (scc_options key) - → scc_enqueue_scripts() fetches and localizes +WordPress DB (warder_options key) + → warder_enqueue_scripts() fetches and localizes → window.sccSettings in browser → createConfigFromSettings() in src/index.js → CookieConsent.run(config) ``` -### PHP Layer (`simple-cookie-consent.php`) +### PHP Layer (`warder-cookie-consent.php`) -All plugin logic is in this single file (~552 lines): +All plugin logic is in this single file (~851 lines): -- **`scc_get_default_options()`** — defines the canonical default settings structure -- **`scc_get_merged_options()`** — retrieves DB options and deep-merges with defaults; always returns a complete settings object -- **`scc_validate_options()`** — sanitizes/validates before saving to `scc_options` in `wp_options` -- **`scc_enqueue_scripts()`** — enqueues `dist/cookieconsent.bundle.js` and localizes it as `window.sccSettings` -- **`scc_settings_page()`** — renders the admin UI at Settings > Cookie Consent -- Settings are versioned via `scc_options_last_updated` timestamp for cache busting +- **`warder_get_default_options()`** — defines the canonical default settings structure +- **`warder_get_merged_options()`** — retrieves DB options and deep-merges with defaults; always returns a complete settings object +- **`warder_validate_options()`** — sanitizes/validates before saving to `warder_options` in `wp_options` +- **`warder_enqueue_scripts()`** — enqueues `dist/cookieconsent.bundle.js` and localizes it as `window.sccSettings` +- **`warder_render_options_page()`** — renders the admin UI at Settings > Cookie Consent +- Settings are versioned via `warder_options_last_updated` timestamp for cache busting ### JS Layer (`src/index.js` → `dist/cookieconsent.bundle.js`) @@ -59,12 +59,24 @@ Scripts are blocked until consent is given via `data-cookiecategory` HTML attrib ### Settings Structure ```php -scc_options = [ - 'general' => [ position, language, ... ], - 'texts' => [ banner title/description, accept/reject button labels, ... ], - 'categories' => [ - 'necessary' => [ enabled, readonly, cookies[] ], - 'analytics' => [ enabled, cookies[] ], +warder_options = [ + 'enabled' => bool, + 'current_lang' => string, + 'autoclear_cookies' => bool, + 'page_scripts' => bool, + 'show_preferences_toggle' => bool, + 'preferences_toggle_position'=> string, + 'title' => string, + 'description' => string, + 'primary_btn_text' => string, + 'primary_btn_role' => string, + 'secondary_btn_text' => string, + 'secondary_btn_role' => string, + 'privacy_policy_url' => string, + 'cookie_categories' => [ + 'necessary' => [ title, description, enabled, readonly, cookies[] ], + 'analytics' => [ title, description, enabled, cookies[] ], + ... // user-defined categories ], ] ``` @@ -75,7 +87,7 @@ Each `cookies` entry supports `name` (exact string or `/regex/` pattern) and `do The `Version:` header in `warder-cookie-consent.php` is the canonical version (this is what WordPress.org reads). When bumping the version, update all of these together: -- `warder-cookie-consent.php` — `Version:` header +- `warder-cookie-consent.php` — `Version:` header and `WARDER_VERSION` constant - `readme.txt` — `Stable tag:` plus new `== Changelog ==` and `== Upgrade Notice ==` entries - `CHANGELOG.md` — new version heading - `package.json` — `version` field (kept in sync even though this package is not published to npm) diff --git a/docs/architecture-plan.md b/docs/architecture-plan.md new file mode 100644 index 0000000..3709f6f --- /dev/null +++ b/docs/architecture-plan.md @@ -0,0 +1,245 @@ +# Warder Cookie Consent - Architecture Plan + +## Current State (v1.5.1) + +### File Structure + +``` +warder-cookie-consent/ +├── warder-cookie-consent.php # All plugin logic (~851 lines) +├── src/index.js # JS entry point (webpack input) +├── dist/cookieconsent.bundle.js# Compiled bundle (webpack output) +├── webpack.config.js +├── languages/ +├── docs/ +└── ...config files +``` + +### What Is Already Implemented + +- Settings saved inline notice (`$_GET['settings-updated']`) inside `warder_render_options_page()` +- Yellow background highlight on changed fields (inline jQuery via `wp_add_inline_script`) +- Show/hide add-cookie form (inline jQuery) +- Activation-time merge of defaults (`warder_plugin_activate`) +- Cache busting via `warder_options_last_updated` timestamp on option update +- Welcome notice on other admin pages (`warder_admin_notices` via `admin_notices` hook) +- Cookie category/cookie add and delete via `warder_handle_admin_actions()` + +### Known Issues + +#### 1. Save UX — Page Jumps to Top +- **Cause**: form POSTs to `options.php`, which redirects back with `?settings-updated=true`, resetting scroll position +- **Impact**: disorienting on a long settings page; user loses context +- **Current partial fix**: inline success notice at top of page content — visible but requires scrolling back + +#### 2. Monolithic File +- **Issue**: all logic in one 851-line file +- **Impact**: harder to navigate and maintain as the plugin grows +- **Not urgent** at current size, but worth planning for v2.0.0 + +--- + +## Phase 1: Fix Save UX (v1.6.0) + +### Recommended Approach: AJAX Save + +Replace the standard `options.php` form submission with an AJAX handler. This eliminates the page reload entirely, keeps the user at their scroll position, and gives immediate visual feedback. + +**Why not the scroll-position-via-POST hack?** +Storing scroll offset in a hidden POST field and restoring it via JS after redirect is fragile, non-standard, and still flickers. AJAX solves the root cause. + +**Why not stay with WordPress Settings API redirect?** +The Settings API redirect to `options.php` is what causes the jump. Intercepting the submit and using `wp_ajax_` directly is cleaner and widely used in modern WP plugins. + +### PHP: AJAX Handler + +Add to `warder-cookie-consent.php`: + +```php +add_action( 'wp_ajax_warder_save_settings', 'warder_ajax_save_settings' ); + +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'] ) ? $_POST['warder_options'] : array(); + $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' ), + ) ); + } +} +``` + +### JS: Intercept Submit + +Replace the current inline `$admin_js` string with an enqueued file (see Phase 2), or extend it inline for now: + +```javascript +jQuery( document ).ready( function( $ ) { + $( '#warder-main-settings-form' ).on( 'submit', function( e ) { + e.preventDefault(); + + var form = $( this ); + var submitBtn = form.find( 'input[type="submit"]' ); + var nonce = $( '#_wpnonce' ).val(); + + submitBtn.prop( 'disabled', true ).val( warderAdmin.saving ); + + $.post( ajaxurl, form.serialize() + '&action=warder_save_settings', function( response ) { + $( '.warder-ajax-notice' ).remove(); + var cls = response.success ? 'notice-success' : 'notice-error'; + form.before( + '
' + + '

' + response.data.message + '

' + ); + if ( response.success ) { + form.find( 'input, textarea, select' ).css( 'background-color', '' ); + } + } ).always( function() { + submitBtn.prop( 'disabled', false ).val( warderAdmin.save ); + } ); + } ); +} ); +``` + +Use `wp_localize_script` to pass translated strings (`warderAdmin.save`, `warderAdmin.saving`) rather than hardcoding them in JS. + +### Fallback (no-JS) +Keep the existing `options.php` form action and `settings_fields()` call intact. The AJAX handler intercepts via `e.preventDefault()` — if JS is unavailable, the form submits normally and the existing redirect notice still works. + +### Changes Required in `warder_enqueue_admin_scripts()` +- Add `wp_localize_script()` call for `warderAdmin` strings after enqueueing jQuery +- Extend `$admin_js` to include the submit intercept, or extract to a file (preferred) + +--- + +## Phase 2: Extract Admin JS to a File (v1.6.0 or v2.0.0) + +Currently all admin JS is a PHP heredoc string passed to `wp_add_inline_script`. This is hard to edit, lint, or test. + +### Change +1. Create `assets/js/admin.js` +2. In `warder_enqueue_admin_scripts()`, replace `wp_add_inline_script` with: + ```php + 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( + 'save' => __( 'Save Settings', 'warder-cookie-consent' ), + 'saving' => __( 'Saving…', 'warder-cookie-consent' ), + ) ); + ``` +3. Move all inline JS (show/hide form, field highlight, AJAX submit) into `assets/js/admin.js` + +No webpack needed — this is plain jQuery, not a module. + +--- + +## Phase 3: File Structure Refactoring (v2.0.0) + +Only warranted if the main PHP file grows significantly beyond its current 851 lines or if the codebase gains contributors. Premature splitting adds navigation overhead. + +### Proposed Structure (when needed) + +``` +warder-cookie-consent/ +├── warder-cookie-consent.php # Plugin header + require loader only +├── inc/ +│ ├── settings.php # warder_register_settings(), warder_validate_options() +│ ├── admin.php # warder_render_options_page(), warder_enqueue_admin_scripts() +│ ├── ajax.php # warder_ajax_save_settings(), warder_handle_admin_actions() +│ ├── frontend.php # warder_enqueue_scripts(), warder_add_preferences_button() +│ └── defaults.php # warder_get_default_options(), warder_get_merged_options() +├── assets/ +│ └── js/ +│ └── admin.js +├── src/ +│ └── index.js # Frontend bundle entry (webpack) +├── dist/ +│ └── cookieconsent.bundle.js +└── ... +``` + +### Migration Steps +1. Create `inc/` directory +2. Move function groups one file at a time, verifying nothing breaks after each move +3. Add `require_once` calls in the main file +4. No class-based refactor needed — procedural functions with `warder_` prefix work fine at this scale + +--- + +## WordPress.org Plugin Guidelines Compliance + +Relevant rules from the [Detailed Plugin Guidelines](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/) and how they apply: + +### Guideline 4 — No Obfuscation +Webpack minification of `dist/cookieconsent.bundle.js` is fine. Variable name mangling (uglify `mangle` option) is **not** permitted. The webpack config must keep readable variable names in the bundle, and `src/index.js` must remain in the repo as the human-readable source. + +### Guideline 11 — Admin Notices Must Be Dismissible +- The AJAX save notice (Phase 1) must include the `is-dismissible` CSS class — already in the proposed markup. +- The existing welcome/setup notice in `warder_admin_notices()` must be dismissible. Currently it is (`is-dismissible`), but it shows on **every** admin page until settings are configured. It should self-suppress once the plugin has been configured (e.g. check `get_option('warder_options_last_updated')`). +- Upgrade prompts or upsell messaging must never appear site-wide — only on the plugin's own settings page. + +### Guideline 13 — Use WordPress Bundled Libraries +- jQuery is bundled with WordPress; always enqueue via `wp_enqueue_script( 'jquery' )`, never bundle or load a separate copy. +- `vanilla-cookieconsent` is not bundled with WordPress, so including it via webpack is correct. It uses the MIT licence, which is GPL-compatible (Guideline 1). +- Do not load any assets from third-party CDNs (Guideline 8). All JS and CSS must be self-hosted in the plugin. + +### Guideline 7 — No User Tracking Without Consent +The AJAX save handler (`warder_ajax_save_settings`) must not transmit any data externally. Settings are saved only to `wp_options` — this is compliant. If any future feature sends data off-site (e.g. telemetry), it requires an explicit opt-in checkbox. + +### Guideline 8 — No Third-Party Code Execution +The plugin must not make external HTTP requests during normal operation. The AJAX handler calls only `update_option()` and `wp_send_json_*()` — fully local. If external requests are ever added (e.g. licence checks), they require disclosure and must be genuinely functional (not solely for licence enforcement). + +--- + +## Security Checklist (All Changes) + +- Verify nonce on every AJAX handler: `check_ajax_referer()` +- Check capabilities: `current_user_can( 'manage_options' )` +- Sanitize all `$_POST` input via `warder_validate_options()` before saving +- Escape all output with `esc_html()`, `esc_attr()`, `esc_url()`, `esc_js()` +- Never trust `$_GET['settings-updated']` for security decisions (it is UI-only) +- No external HTTP requests in AJAX handlers (Guideline 8) + +--- + +## Testing Checklist + +- [ ] Settings save without page jump (AJAX path) +- [ ] Success/error notice appears next to form, includes `is-dismissible` class (Guideline 11) +- [ ] Changed-field highlight resets on successful save +- [ ] Fallback: settings save normally when JS is disabled +- [ ] Welcome notice suppresses itself after plugin has been configured (Guideline 11) +- [ ] Add/delete category and cookie still work (not affected by AJAX save change) +- [ ] All existing options survive a round-trip through `warder_validate_options()` +- [ ] No external HTTP requests fired during save (Guideline 8) +- [ ] Works on WordPress multisite +- [ ] Compatible with PHP 8.0+ + +--- + +## WordPress Coding Standards + +All changes must follow: +- [WordPress PHP Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/) +- [WordPress JavaScript Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/js/) +- [WordPress CSS Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/css/) +- [WordPress.org Detailed Plugin Guidelines](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/) + +Lint PHP with `vendor/bin/phpcs --standard=phpcs.xml warder-cookie-consent.php` before committing. From 34452c4a29e461f2deb7773097b43bd7c55f11ef Mon Sep 17 00:00:00 2001 From: Jasper Frumau Date: Thu, 28 May 2026 10:51:26 +0700 Subject: [PATCH 02/10] feat: add AJAX settings save and extract admin JS to file Add warder_ajax_save_settings() AJAX handler that verifies the nonce and capability, runs input through the existing warder_validate_options(), and returns a JSON success/error response so the page never reloads on save. Extract the inline $admin_js heredoc to assets/js/admin.js and enqueue it as a proper script handle (warder-admin) with a jquery dependency. Add the WARDER_VERSION constant and use wp_localize_script to pass the ajaxurl and translated button-label strings (warderAdmin.save / warderAdmin.saving) instead of hardcoding them. The no-JS fallback via options.php is preserved. --- assets/js/admin.js | 45 +++++++++++++++++++++++++++++ warder-cookie-consent.php | 61 +++++++++++++++++++++++++++------------ 2 files changed, 87 insertions(+), 19 deletions(-) create mode 100644 assets/js/admin.js diff --git a/assets/js/admin.js b/assets/js/admin.js new file mode 100644 index 0000000..d04715d --- /dev/null +++ b/assets/js/admin.js @@ -0,0 +1,45 @@ +/* global warderAdmin */ +jQuery( document ).ready( function( $ ) { + + $( '.show-add-cookie-form' ).on( 'click', function() { + var categoryId = $( this ).data( 'category' ); + $( '#warder-add-cookie-container-' + categoryId ).show(); + } ); + + $( '.cancel-add-cookie' ).on( 'click', function( e ) { + e.preventDefault(); + $( this ).closest( '.warder-add-cookie-form-container' ).hide(); + } ); + + $( '#warder-main-settings-form input, #warder-main-settings-form textarea, #warder-main-settings-form select' ).on( 'change', function() { + $( this ).css( 'background-color', '#ffffdd' ); + } ); + + $( '#warder-main-settings-form' ).on( 'submit', function( e ) { + e.preventDefault(); + + var form = $( this ); + var submitBtn = form.find( 'input[type="submit"]' ); + + submitBtn.prop( 'disabled', true ).val( warderAdmin.saving ); + + $.post( + warderAdmin.ajaxurl, + form.serialize() + '&action=warder_save_settings', + function( response ) { + $( '.warder-ajax-notice' ).remove(); + var cls = response.success ? 'notice-success' : 'notice-error'; + form.before( + '
' + + '

' + response.data.message + '

' + ); + if ( response.success ) { + form.find( 'input, textarea, select' ).css( 'background-color', '' ); + } + } + ).always( function() { + submitBtn.prop( 'disabled', false ).val( warderAdmin.save ); + } ); + } ); + +} ); diff --git a/warder-cookie-consent.php b/warder-cookie-consent.php index 7af0245..403e999 100644 --- a/warder-cookie-consent.php +++ b/warder-cookie-consent.php @@ -16,6 +16,8 @@ defined( 'ABSPATH' ) || exit; +define( 'WARDER_VERSION', '1.5.1' ); + /** * Registers plugin settings and adds default options on first activation. */ @@ -182,27 +184,48 @@ function warder_enqueue_admin_scripts( $hook ) { return; } - wp_enqueue_script( 'jquery' ); - - $admin_js = ' -jQuery(document).ready(function($) { - $(".show-add-cookie-form").on("click", function() { - var categoryId = $(this).data("category"); - $("#warder-add-cookie-container-" + categoryId).show(); - }); - $(".cancel-add-cookie").on("click", function(e) { - e.preventDefault(); - $(this).closest(".warder-add-cookie-form-container").hide(); - }); - $("#warder-main-settings-form input, #warder-main-settings-form textarea, #warder-main-settings-form select").on("change", function() { - $(this).css("background-color", "#ffffdd"); - }); -});'; - - wp_add_inline_script( 'jquery', $admin_js ); + 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. * @@ -574,7 +597,7 @@ class="regular-text warder-category-title-field" + inputs reference it via the HTML5 `form` attribute so they aren't nested). -->