Ravanix — full changelog
=========================

The most recent versions are kept in readme.txt (Changelog section);
this file holds the complete history.

= 1.4.2 =
* New: Ravanix_Shortcodes::render_test_list() accepts two new $atts -- 'limit' (0, default, means no LIMIT clause; otherwise intval()'d and passed through $wpdb->prepare()) and 'order' ('newest', default, ORDER BY id DESC; or 'random', ORDER BY RAND() -- chosen from this fixed pair of hardcoded SQL fragments, never built from the raw attribute string, so there's nothing for it to inject). Applies before the layout branch, so render_test_titles() (added in 1.4.1) benefits automatically along with the grid/list card layouts
* Changed: class-ravanix-block.php's attributes array and render() gained matching limit/order entries (same defaults), passed through to render_test_list() unchanged; assets/js/ravanix-block.js's editor gained a RangeControl ("Maximum number to show", 0-24, help text explaining 0 = all) and a SelectControl ("Order": Newest first / Random), both shown for mode === 'list' regardless of which of the three layouts is selected, unlike the layout-specific controls

= 1.4.1 =
* New: 'titles' is now a valid value for [ravanix_test_list layout="..."] (and the matching Gutenberg block option, "Titles only") -- Ravanix_Shortcodes::render_test_list() branches to a new render_test_titles() for it: a plain <ul> of just each published test's title (linked to its custom-post-type page if one exists, plain text otherwise -- deliberately no image, excerpt, button, or the admin-only "no dedicated page yet" hint the card layouts show, to stay minimal), each marked with Ravanix_Shortcodes::get_titles_layout_icon() (a small inline SVG, currentColor stroke, same convention as the meta-row icons in templates/frontend-test.php) instead of a bullet
* New: .rs-test-list-titles / .rs-test-titles(-icon) in ravanix-frontend.css -- icon color from --md-primary (derives from the admin's brand_color), logical properties throughout, focus-visible on the title links
* Changed: assets/js/ravanix-block.js's Layout control has a third "Titles only" option; the column-count RangeControl and the image/excerpt ToggleControls are hidden while it's selected, since none of them apply to this layout

= 1.3.10 =
* Fixed: Ravanix_Admin::enqueue_assets() compared $hook against a hardcoded 'ravanix_page_ravanix-settings' guess to decide whether to load ravanix-settings.css/.js and call wp_enqueue_editor(). WordPress actually builds a top-level page's hook-name component from sanitize_title() of add_menu_page()'s *menu title* argument (translatable), not the literal $menu_slug -- so on a site with a Persian translation for the string "Ravanix" active, the real hook looked like "{sanitized-persian-title}_page_ravanix-settings", the hardcoded comparison never matched, and this page's CSS/JS simply never loaded (confirmed via a reporting user's own console: document.styleSheets/document.scripts both came back empty for 'ravanix-settings', and document.body.className contained the Persian translation UTF-8-byte-encoded). Ravanix_Admin now has a $settings_hook property, set from add_submenu_page()'s own return value for the Settings page in register_menu() (which always runs before enqueue_assets(), same object instance), and compared against instead of any guessed string
* Note: the broader `strpos( $hook, 'ravanix' ) === false` gate (used here and in both of Ravanix Pro's admin_enqueue_scripts callbacks) was never affected by this -- only the translated *parent* portion of the hook changes; every submenu's own slug (e.g. 'ravanix-settings') stays the literal, untranslated string passed to add_submenu_page(), so a substring check for "ravanix" still matches regardless

= 1.3.9 =
* Fixed: assets/js/ravanix-settings.js's initEditorsForTab() now wraps its wp.editor.initialize() call in try/catch, and the DOMContentLoaded handler wraps tab-restoration and the "copy shortcode" button setup in their own try/catch blocks too, with tab-button click listeners attached before any of that (so they're registered even if everything after them throws). Root cause on the reporting site: a Content Security Policy blocking eval() broke TinyMCE's RTL/directionality language pack (only loaded for an RTL admin locale), and without isolation that uncaught exception prevented the rest of this file's initialization -- tabs appeared as plain, unstyled, non-functional buttons with every section stacked below them, on RTL admin locales only
* Changed: assets/css/ravanix-settings.css's toggle-switch RTL flip now also matches body.rtl (the class WordPress core adds to <body> for an RTL admin locale), not just this page's own [dir="rtl"] wrapper attribute -- consistent with the convention core and other plugins' admin CSS already use

= 1.3.8 =
* New: admin/views/settings.php rebuilt around 4 tabs (data-tab="general|notifications|users|tools", pure client-side show/hide -- see assets/js/ravanix-settings.js) with card-grouped sections (assets/css/ravanix-settings.css, enqueued only on this page). Every existing field kept its exact name/id/value handling; only the surrounding markup changed
* New: Ravanix_Admin::enqueue_assets() calls wp_enqueue_editor() and enqueues ravanix-settings.css/.js, conditionally on the 'ravanix_page_ravanix-settings' hook only
* Changed: the three rich-text fields on this page (consent_text, notify_admin_body, notify_participant_body) are plain <textarea> elements in PHP; ravanix-settings.js calls wp.editor.initialize() on each one itself, exactly when its tab becomes the active one (including on page load, for whichever tab that happens to be) -- not through wp_editor() directly, since TinyMCE does not reliably size itself the first time it initializes inside a display:none container, which every inactive tab is. The two notification-enabled toggles no longer hide their own subject/recipient/message fields via JS for the same reason (nested hidden containers around a lazily-initialized editor); they're always visible now, matching the same choice already made for Ravanix Pro's per-test notification overrides
* Changed: Ravanix_Admin_Handlers::save_settings()'s redirect now carries active_tab from the submitted form back through the URL, so ravanix-settings.js can restore the tab the admin was on when they saved, instead of always landing back on "General"
* Improved: the roles matrix (Roles & Permissions tab) is wrapped in .rs-table-scroll-x, since wp_roles() is dynamic -- a site with WooCommerce or a membership plugin can have far more than the handful of default roles this table's width was originally sized around

= 1.3.7 =
* Fixed: Ravanix_Roles::ensure_roles() only granted the ravanix_manager role 'read' plus its Ravanix-specific capabilities; added 'edit_posts' too, since that's the capability WooCommerce's own WC_Admin (and similar plugins) check to decide whether to redirect a user away from wp-admin to the frontend account page as a "mere customer". ensure_roles() now also re-applies the full capability set to the role every time it runs (not just at role-creation time), so a site whose ravanix_manager role was already created under 1.3.6 gets this fix automatically on upgrade, without needing to delete and recreate the role
* Changed: notify_admin_body/notify_participant_body (Settings → Email notifications) render via wp_editor() (Classic Editor) instead of a plain <textarea>, and are sanitized with wp_kses_post() instead of sanitize_textarea_field() on save, matching the existing consent_text pattern
* Changed: Ravanix_Notifications::send() now sends with a 'Content-Type: text/html' header and passes the body through wpautop() first, instead of sending as plain text
* Changed: Ravanix_Notifications::build_placeholders()/send_admin_notification() now esc_html() (test_title, site_name, submitted_at, participant_name) or esc_url() (admin_result_link) every value before it's substituted into a template, since the template is now HTML -- participant_name in particular can come directly from a participant's own submitted "full name" field
* Changed: default_admin_body()/default_participant_body() rewritten as HTML (<p>/<br>) instead of plain text with \n separators, so they display correctly the first time an admin opens the rich-text editor

= 1.3.6 =
* New: Ravanix_Roles (includes/class-ravanix-roles.php) -- 6 capabilities (ravanix_manage_tests, ravanix_view_results, ravanix_delete_results, ravanix_export_results, ravanix_import_export_tests, ravanix_manage_settings) plus one auto-managed umbrella capability (ravanix_access, controls only whether the top-level admin menu is visible; recomputed whenever the matrix below changes a role's other capabilities). Administrator gets all of them via ensure_roles(), called from Ravanix_Activator::activate() (idempotent, so a future version adding a new capability reaches existing sites on their next upgrade too, no reactivation needed)
* New: a 'user_has_cap' filter grants every Ravanix capability to any user who already has manage_options -- this is what actually guarantees no lockout, including for WordPress's own internal current_user_can() calls (e.g. deciding admin menu visibility) that this plugin has no other way to wrap
* New: ready-made "ravanix_manager" role (Settings → Roles & Permissions describes it as "Ravanix Manager"), granted ravanix_manage_tests + ravanix_view_results + ravanix_access by ensure_roles()
* New: Settings → Roles & Permissions -- a checkbox matrix (6 capabilities x every real registered WP role from wp_roles(), Administrator shown fixed/disabled) saved via Ravanix_Roles::save_matrix(), submitted through the same form/nonce as the rest of Settings
* Changed: admin menu registration (class-ravanix-admin.php) and Ravanix_Admin_Handlers::check() (now takes an optional $capability parameter, defaulting to ravanix_manage_tests) use the new capabilities instead of a flat manage_options; ravanix_delete_result and the results-list bulk action require ravanix_delete_results specifically, ravanix_save_settings requires ravanix_manage_settings
* Changed: admin/views/results-list.php only renders the bulk-select checkboxes and each row's "Delete" link for a user who actually has ravanix_delete_results (previously: unconditionally, then blocked server-side on submit)
* Changed: uninstall.php (opt-in erase path only) now also removes every Ravanix capability from every role and deletes the ravanix_manager role, reimplemented directly against $wpdb/core role functions rather than requiring class-ravanix-roles.php, matching this file's existing standalone-execution design

= 1.3.5 =
* New: two extension points in the email-notification system for Ravanix Pro to use -- 'ravanix_notification_settings' (per-test recipient/subject/body/on-off overrides, applied in Ravanix_Notifications::maybe_send() just before it acts on the site-wide settings) and 'ravanix_notification_placeholders' (extra {tokens}, applied in build_placeholders(); Pro uses this for its opt-in {scores_summary}). Neither filter has a callback added to it by Lite itself
* New: 8 new nullable/defaulted columns on ravanix_tests (notify_admin_mode, notify_admin_emails_override, notify_admin_subject_override, notify_admin_body_override, notify_admin_condition, notify_participant_mode, notify_participant_subject_override, notify_participant_body_override) -- inert unless Ravanix Pro 1.2.4+ is active and a test's mode is changed from the 'default' every column defaults to, so existing tests keep behaving exactly as before this update

= 1.3.4 =
* New: Ravanix_Notifications (includes/class-ravanix-notifications.php), hooked to a new 'ravanix_result_finalized' action fired in Ravanix_Ajax::submit_test() right after Ravanix_Scoring::save_result() returns -- deliberately later than and separate from 'ravanix_after_save_result' (fired *inside* save_result(), which Pro uses to attach T-scores/composite factors), so notification behavior never depends on hook-priority ordering against Pro
* New: 7 new Settings fields (notify_admin_enabled/emails/subject/body, notify_participant_enabled/subject/body) under a new "Email notifications" section; all off by default

= 1.3.3 =
* Improved: .rs-columns gap reduced from 30px to 15px, and a new @media (min-width: 783px) and (max-width: 1100px) breakpoint gives the form column 45% (was a flat 40% at every width down to the 782px stack point) -- reuses wp-admin's own two menu breakpoints (960px icon-collapse, 782px mobile toggle) rather than introducing unrelated ones

= 1.3.2 =
* Improved: Ravanix_DB::get_full_test() now loads dimensions/questions/options/interpretations/norms/composites with a fixed, small number of batched "WHERE id IN (...)" queries instead of one query per parent row, and caches the assembled structure (object cache, with a transient fallback on sites without an external object cache); the cache is versioned by the test's own updated_at and automatically invalidated whenever a dimension, question, interpretation, norm, or composite belonging to it is saved or deleted
* Improved: activation/upgrade no longer always re-syncs every existing test with the custom post type and flushes rewrite rules synchronously in the same request; above 20 existing tests, this now runs once in the background (Action Scheduler if available, otherwise wp-cron) to avoid a PHP timeout on large sites, matching the same deferred pattern already used by the "catch-up sync" when CPT display is newly enabled in Settings
* Security: guest participant tokens (ravanix_guest_token cookie) are now generated with random_bytes(), PHP's CSPRNG, instead of wp_generate_password(); previously-issued tokens are unaffected and keep working
* Fixed: an interpretation range's admin-chosen level_color is drawn as a background behind fixed white badge text in several places (frontend result page, admin result view, Ravanix Pro's PDF/CSV/trend views); a light color (or the previous raw '#999999' fallback, itself only ~2.85:1) could fail WCAG AA contrast. It now passes through the same ensure_min_contrast_with_white() check already used for the brand color, applied where the score is computed rather than to the admin's saved setting
* Fixed: same layout problem as the .rs-table-scroll-x fix below, but in the two-column "Add New.../list" layout itself (shared by Dimensions, Questions, Interpretation Ranges, and Ravanix Pro's Norms/Composite Factors): Ravanix_DB::get_full_test()'s .rs-col-form was flex: 0 1 288px next to .rs-col-list's flex: 1, which stayed a small fixed width on wide screens while the list column claimed everything else. .rs-columns is now display:grid with grid-template-columns: 40% 60%, holding that ratio at any width above the existing single mobile breakpoint (which still stacks to one column)
* Fixed: .rs-table-scroll-x (overflow-x:auto safety net, previously only on the Dimensions tab) is now also on the Questions and Interpretation Ranges tables, so a table wider than its 60% column scrolls internally instead of overflowing the page
* Fixed: two WCAG AA contrast failures on the "Upgrade to Pro" admin screen — the button-hero CTA's white-on-gold text (~2.2:1) and the ✓/✕ comparison-table icons (~1.6–2.6:1)
* Fixed: a database error while saving a test (admin_post_ravanix_save_test) showed the raw $wpdb->last_error text to the admin; it now shows a generic message and logs the real error under WP_DEBUG only

= 1.3.1 =
* Fixed: the "Dimensions of this test" / "Questions in this test" tables (and the same layout on Interpretation Ranges, and Ravanix Pro's Norms and Composite Factors tabs) needed horizontal scrolling on many screens; the "Add New..." form column next to them is now 20% narrower and no longer expands to steal their space, so those tables fit without scrolling on far more screen sizes

= 1.3.0 =
* New: the plain "Number of questions" text on a test's intro is now a 3-item table (question count, estimated completion time, total participant count so far), each with its own icon
* Improved: the "Start Test" and "Submit answers" buttons are now noticeably larger and more prominent, as the two most significant actions on the page
* Fixed: "My Results", the questionnaire archive, and the single-questionnaire page never declared their own background color -- only their text color adapted to dark mode, while the background stayed whatever the surrounding theme/page provided (usually light), which could make table cells and other content in those specific views hard to read on a device with dark mode active. (The 1.2.3 fix for this same class of bug covered the test-taking page itself, but missed these other three views.)
* Fixed: the Dimensions tab's table (Name, Code, Number of questions, Quick-assign, Actions) could overflow the page horizontally; the quick-assign field is narrower now, and the table has a safety-net horizontal scroll of its own rather than the whole admin page scrolling

= 1.2.3 =
* Fixed: tables (My Results, composite scores) could become unreadable -- dark background with equally-dark text, or the reverse -- on a device/browser with a "force dark mode for web content" setting (common on some mobile browsers), because the page never told the browser it already handles dark mode itself; color-scheme: light dark is now declared, and table cells also set their text color explicitly rather than relying only on inheritance

= 1.2.2 =
* Fixed: enabling "Display as a custom post type" in Settings only ever created the linked post for a test the next time that specific test was individually opened and re-saved -- a test created (or already published) before the setting was turned on never got synced automatically. Turning the setting on now catches up every published test in that same request.

= 1.2.1 =
* Fixed: an admin-chosen brand color that was too light could make white button text (and the color's own use as question-number/link text) fail WCAG AA contrast; a real WCAG relative-luminance check now automatically darkens a color only as much as actually needed, computed fresh on every page load -- the saved setting itself is never changed
* Fixed: in dark mode, the default color scheme's button text measured only ~2.6:1 contrast against its own (deliberately lightened) button background, well under the 4.5:1 minimum; both the default and any custom brand color now get a dark-mode text color chosen by actually measuring contrast rather than a fixed color blend

= 1.2.0 =
* New: the front-end test-taking and results pages (and Ravanix Pro's My Results dashboard, which shares this stylesheet) now use a Material Design 3-inspired visual system -- MD3 color roles, shape scale, simplified type scale, and CSS-only state layers on buttons/answer selections
* New: a "Brand color" setting (Ravanix Settings -> Branding) lets the admin pick one accent color; every other color in the front-end palette is derived from it automatically via the browser's own color-mix() function, no extra library or build step
* New: automatic light/dark appearance, following the visitor's OS/browser preference (prefers-color-scheme)
* This is a CSS-only redesign: no HTML/JS structure changed, so no database migration and no impact on existing shortcodes, custom post type pages, or stored results

= 1.1.5 =
* Fixed (HIGH): the server accepted any numeric value for a submitted answer without checking it was actually one of that question's real choices for every question type (Likert-5/7, Yes/No, custom multiple-choice, and Pro's forced-choice questions), so a tampered request could submit e.g. "999" for a 5-point Likert item and corrupt that dimension's raw score/percentage
* Fixed: a "Powered by Ravanix" link was always shown on every result page with no way to turn it off (a WordPress.org Guideline 10 violation); it is now off by default, with an opt-in "Show branding" toggle in Ravanix Settings
* Fixed: a question's "show only if" (skip logic) source question can no longer be set to one that comes later in display order, which could never actually be satisfied; this is now enforced when saving a question
* Fixed: readme.txt incorrectly described text direction (RTL/LTR) as a per-test setting; it actually follows the site's language (WordPress's own is_rtl()) site-wide, not per test
* Improved: restoring a saved draft no longer builds a jQuery selector directly from the saved value, so an unusual/tampered browser-local draft can no longer break the whole restore -- values are now compared via JS equality instead
* Improved: reworded an internal code comment away from the word "tracking" to more accurately describe its actual, narrow purpose (guest-session identification and anti-abuse controls)
* Added: a Privacy section in readme.txt, documenting exactly what's stored for a guest or logged-in participant and why

= 1.1.4 =
* Fixed: resuming a saved draft (Save & Resume) could skip the informed-consent step entirely, since the "Resume where you left off" button never checked whether consent had been agreed to
* Fixed: comparing a server-saved draft against the browser's local one used the wrong timezone (a site-local timestamp was read back as if it were already UTC), which could make a genuinely older draft look newer than it was, or vice versa
* Fixed: deleting a question that another question's "show only if" (skip logic) condition depended on left that other question permanently, silently hidden with no way to ever satisfy its condition again; the reference is now cleared automatically when the depended-on question is deleted
* Fixed: an already-defined but never-actually-used debounced autosave function is now correctly wired up to the per-answer autosave, avoiding redundant localStorage writes on rapid successive clicks

= 1.1.3 =
* Fixed: several responsive/mobile layout bugs in the admin panel — a fixed-width form column that didn't shrink to fit narrow screens (Dimensions, Interpretation Ranges, and the question edit form all share this layout), the bulk-import row's dropdowns/inputs overflowing the viewport, WordPress core's own fixed-width .regular-text/.large-text classes not fitting phone-width screens, and two custom admin tables missing the data-colname/toggle-row markup WordPress's own responsive table CSS needs (Participant info fields, and Participant Results, which had the same bug though it wasn't reported)
* Improved: consistent spacing/alignment for checkbox-plus-label pairs across every admin form (e.g. the "Randomize order" checkboxes)
* Improved: the built-in sample questionnaire ("Sample: Mood Screening Scale") now demonstrates every feature in this version of the plugin — branching/skip logic, a custom per-test informed-consent notice, Save & Resume, all four question types (Likert-5, Likert-7, Yes/No, custom multiple-choice), importance weighting, pagination, ranked results, and randomized answer-option order — instead of only the original two Likert-5 dimensions

= 1.1.2 =
* Improved: readme.txt rewritten to current WP.org guidelines — Tags trimmed to the current limit of 5 relevant, non-generic terms (RTL dropped as redundant), and version history split into changelog.txt to keep this file under the recommended size
* Fixed: Ravanix Pro's Plugin URI no longer duplicates this plugin's own, per official guidance against reusing a URI across a free/paid pair

= 1.1.1 =
* Fixed: Plugin Check warnings introduced by 1.1.0's Save & Resume feature — a misplaced phpcs suppression comment on the wrong line in the draft lookup query, missing sniff codes for the new $wpdb->delete()/->replace() calls, and an unsanitized read of the consent_agreed field
* Fixed: a stale, incorrect header comment in class-ravanix-ajax.php that described a nonce-checking pattern from a different file, not this one

= 1.1.0 =
* New: informed consent — a site-wide default notice (Ravanix Settings) with a per-test override or opt-out, shown collapsed before "Start Test" and enforced both client- and server-side
* New: Save & Resume — every participant's answers are always autosaved in their browser; the admin can additionally enable a visible "Save my progress" button per test, which also lets a logged-in participant resume on a different device
* New: branching / skip logic — a question can be set to only appear if a specific earlier question was answered a specific way
* Fixed: with skip logic, a question hidden by its branch condition is no longer submitted or included in that question's dimension's score, consistent with how the scoring engine already treats any other unanswered question

= 1.0.14 =
* Fixed: Plugin Check warnings on uninstall.php — the phpcs suppression comments were targeting the wrong sniff codes (WordPress.DB.PreparedSQL.NotPrepared instead of the actually-firing WordPress.DB.PreparedSQL.InterpolatedNotPrepared and WordPress.DB.DirectDatabaseQuery.SchemaChange) and two loop variables lacked the plugin's prefix
* Fixed: the same missing InterpolatedNotPrepared suppression on the CREATE TABLE statements in the activator (audited the entire codebase; no other occurrences found)

= 1.0.13 =
* Fixed: on left-to-right sites, the "Feature" column header on the Upgrade page's comparison table was right-aligned instead of left-aligned (audited every other table for the same issue; none found)
* Improved: the test title field on the "Add/Edit Test" screen is now noticeably wider than the slug field below it
* Improved: rounded corners applied consistently to text inputs, textareas, and dropdowns across every Ravanix admin screen

= 1.0.12 =
* New: an opt-in "Delete data on uninstall" setting (Settings -> Danger zone, off by default). Only when enabled does removing the plugin permanently delete its questionnaires, results, and settings; otherwise everything is left in place for a future reinstall
* New: integrates with WordPress's own Tools -> Erase Personal Data screen, so a request for a specific logged-in user anonymizes their identifying details while keeping their anonymous dimension scores for aggregate statistics
* Fixed: Plugin URI now points to the plugin's own page instead of the Pro add-on's page

= 1.0.11 =
* Removed the RSS feed calls to psykey.ir on the Settings page (no external requests remain)
* Fixed a missing/doubled separator between the "View profile", "PDF" (Pro), and "Delete" row actions on the "Participant Results" screen
* Tested up to WordPress 7.1

= 1.0.2 =
* New: an optional "Show results ranked from highest to lowest" setting per test, for questionnaires where relative ranking between dimensions matters more than each one's absolute level (e.g. strengths, interests, dominant traits)
* New: dimensions without an interpretation range now show their own general description instead of a blank "not defined" message
* New: test display direction (RTL/LTR) is now detected automatically from the site's language, removing a manual per-test setting
* New: the "All Tests" admin screen now follows WordPress's native list-table conventions (status filter, hover row actions, mobile-responsive layout)

= 1.0.1 =
* Full English translation of the admin interface and all plugin strings; the source .pot file was regenerated accordingly
* Fixed a bug where a participant's gender was matched against a translated display label instead of a stable internal value, which could silently break gender-based norm matching on non-default languages
* Various WordPress coding-standards and documentation clean-ups

= 1.0.0 =
* Initial public release of Ravanix
