=== OmniDirectory – Changelog ===

Full change details. readme.txt lists a short summary of the current and previous release.

== Changelog ==

= 4.3.33 =
* Fix: live search no longer fails with an error on pages served from a full-page cache (an expired nonce, or a cached page shown to a logged-in visitor or after a new login). The page now fetches fresh security tokens once, from the new `omni_refresh_nonces` AJAX action, and retries the search once. The security check itself is unchanged. The profile language switcher recovers the same way.
* Fix: the search rate limit (30 requests per 10 seconds) no longer blocks a visitor searching steadily below that rate, cannot be exceeded by parallel requests, and can no longer be bypassed by changing the browser User-Agent: anonymous visitors now also share a higher per-IP limit (90 per 10 seconds).
* Security: the profile language-switcher AJAX action (`omni_get_translated_member_data`) is now rate limited (60 requests per minute per visitor).
* Fix: CSV export now writes member names exactly as stored. Previously, characters such as `&` and quotes were exported as HTML entities and a re-import saved those entities as the new name.
* Security: CSV export now protects against spreadsheet formula injection: a cell starting with `=`, `+`, `-`, `@`, tab or carriage return is prefixed with an apostrophe. Phone numbers such as `+46 70 123 45 67` and plain numbers are not changed. The prefix is removed again when the file is imported, so every value survives export and re-import unchanged.
* Fix: importing a CSV no longer turns draft, pending or private members into published ones, and no longer publishes a password-protected member without its password. Exports now include "Status" and "Password Protected" columns (the password itself is never exported). For files without them (older exports), a row keeps the status and password of the existing member with the same name; a row whose status or protection can't be kept safely is not imported and is listed on the Import/Export page with the reason.
* Fix: re-importing an export no longer leaves a duplicate of every draft, pending or private member. Unpublished members that are not in the file are kept.
* Import details: a row that is not imported never removes the existing member with the same name (and, with a multilingual plugin, the same language). With Polylang, that member's original translations are kept with it too, so its language switcher keeps working. A Status column in a hand-made file is used only if at least one row holds a real status (publish, draft, pending, private) — otherwise (e.g. "Active") it is ignored as before; once used, an unknown value such as a typo rejects that row instead of publishing it. Scheduled members are never retired by an import (exports don't include them). With a multilingual plugin, a row only replaces an unpublished member in the same language. Imported draft, pending and private members now get their featured image too.
* Import reliability: the background import's lock is renewed by the number of members tried, not only those that succeeded, so a slow or refused change can no longer let a second import run start in parallel.
* Import reliability: a previous member that cannot be moved to Trash (for example because another plugin prevents it) no longer keeps the import from finishing. It is tried once, left in place, and listed with a warning on the Import / Export page.
* Import reliability: a new member whose status cannot be set (for example because another plugin or a database error refuses the update) no longer keeps the import from finishing. It is tried once, stays private (it is not published), and is listed with the reason and its status from the file (including whether it was password-protected) on the Import / Export page. The existing member that its row was matched with at import time (same name and, with Polylang, same language) is kept exactly as it was — not moved to Trash — and the warning says so; nothing is guessed for a member staged before this link was recorded. With Polylang, that member's whole original translation group (as Polylang links it, recorded before anything is replaced) is kept with it, including translations that the file replaced successfully or does not contain, so the kept member's language switcher keeps working; the warning lists those members and notes that a successfully replaced translation then exists in both its old and new version.
* Fix (Polylang): CSV import now applies each row's Language Code to the imported entry (previously every imported entry got the site's default language, so a re-export showed that language for every entry), and links translations again. Exports have a new last column, "Translation Group": entries that are translations of each other share one value. A file that uses a code that is not a language of the site (for example `xx`, or a language that has not been added yet) is refused before the import starts, with the codes listed, and nothing in the directory changes; empty codes are accepted, and codes are not case-sensitive (`AR` = `ar`). Without Polylang set up for directory entries (including WPML sites), a file with any Language Code or Translation Group value is refused before anything changes; files with these columns empty import as before. If a language is removed, or Polylang deactivated, while an import is running, the import stops before the directory is changed. A group is linked only when all its entries were imported, with one per language; otherwise a warning is shown and nothing is linked. Files without the column (older exports) are never linked automatically. Categories created by the import get the entry's language. WPML: languages and links are not applied.
* Fix (Polylang): the Directory Members screen's counts (All, Mine, Published, Scheduled, Draft, Pending, Private, Trash) now match the members listed when a language is selected in Polylang's admin language filter. Previously they counted every language, including members with no language, so for example "Trash (5)" could show above a single listed member, and "Trash (4)" above an empty list after deleting it. As in WordPress, a status with no members in the selected language is not shown, and "Mine" is not shown when it is 0 or equal to "All". With "All languages" selected, without Polylang, and on every other screen (Posts, Pages, menus, the dashboard), counts are unchanged; no member data is changed. Known limitation: "All" and "Mine" keep the wording WordPress chose for the all-language count and only their number changes, so in a language with several plural forms that wording may not match the new number (no difference in English).
* Developers: for files in the 4.3.33 export format (Status and Password Protected columns), the `$row` passed to `omnidirectory_csv_row_meta_input` now holds the values with the export's formula-protection apostrophe removed; older files are passed exactly as written.
* Rate limiting stores small counters in the options table; they are cleaned up automatically, including on sites with a persistent object cache. IPv6 visitors share their /64 network's per-IP limit. On database engines that cannot run the counter query (for example the SQLite integration), the limits are skipped and requests are allowed.
* Reliability: an import in which no row can be imported now stops without changing the directory, and the reason is shown on the Import/Export page.
* Security: a password-protected member's profile now shows only WordPress's password form until the password is entered; its details are no longer printed above the form, in the page's structured data (JSON-LD), or returned by the `omni_get_translated_member_data` AJAX action.
* Security: the `?member_lang=` profile parameter and the language-switcher AJAX action only show a translation that is published and not password-protected. Previously a linked draft, pending, private or password-protected translation could be viewed through them.
* Security: click-to-reveal now only turns its hidden value into a `tel:` or `mailto:` link. Previously any element with its classes, including one in post content written by a user without the `unfiltered_html` capability, could be made into a `javascript:` link.
* Security: the profile's structured data (JSON-LD) now writes `<`, `>` and `&` as JSON escapes, so a member field containing `<!--<script` can no longer break the rest of the page.
* Fix: CSV files are now written and read with standard CSV quoting (no backslash escape character). With PHP's default, a value containing `\"` ended its cell early in spreadsheet programs, so text after it could become a separate, unprotected formula cell, and a value ending in a backslash merged the following rows into it when the file was re-imported. Files without backslashes are read exactly as before.
* Import reliability: a row whose new member cannot be saved (for example because another plugin or the database refuses it) is now listed with the reason, and the existing member it would replace, with its translations, is kept instead of being moved to Trash. A new member is no longer treated as imported unless its status and password information was saved, so it can never be published without them. A kept password that contains a backslash is no longer changed by the import (previously "a\b" became "ab", and a password made only of backslashes was removed).
* Security: a member's Documents field only accepts an attachment the saving user is allowed to view (`read_post`). Previously anyone who can edit a member (for example a Contributor editing their own draft) could attach another user's private or draft file by its ID, which showed its name and URL. Documents already on the member are kept whoever saves it.
* Fix: CSV import maps every column to at most one field. A custom field label (Fields 1-5, Phone) that equals one of this plugin's fixed columns, such as "Status" or "Password Protected", can no longer hide that column: an export with the header twice is read back in column order, and a file with only one such column reads it as the fixed column and says so in a warning. A file with more same-named columns than can be told apart is refused before anything changes.
* Privacy: the uploaded import file (stored as `uploads/omnidirectory-imports/import-<id>.dat`) is now deleted when its import completes or fails, and when the next import is started; older leftover files are removed the same way after a day. A file is never deleted while its import can still be resumed. The directory is protected by `.htaccess` on Apache; on Nginx, add a rule denying `/wp-content/uploads/omnidirectory-imports/`.
* Import reliability: the background import's lock is now taken atomically. Two background runs starting at the same moment could previously both hold it and stage the same rows twice, publishing duplicate members (reproduced: 40 rows became 68 to 76 members). A run also re-checks that it still holds the lock right before saving the job record, so a run that stalled and lost the lock can no longer overwrite the record of the run that took over.
* Import reliability: an error thrown by another plugin while a previous member is moved to Trash, or while a new member's status is set, is now handled like a refused change (tried once, listed with a warning). Previously the swap stopped on that member on every run, the import never finished, and no new import could be started.
* Import reliability: the featured-image worker renews its lock before every image, so a batch of slow downloads can no longer outlast the lock and overlap another import step.
* Readme: Click-to-Reveal is described accurately (a deterrent; the profile's JSON-LD still lists phone and email), with how to remove them using the `omnidirectory_structured_data` filter.
* Performance: CSV export loads member data in a few queries per batch of members instead of several queries per member.
* Added `omnidirectory_csv_export_columns` and `omnidirectory_csv_export_value` filters for companion plugins to add their own columns after this plugin's columns. They cannot change or remove this plugin's columns. No effect by themselves.
* Added `omnidirectory_rate_limits` and `omnidirectory_client_ip` filters (rate-limit values; real visitor IP behind a trusted reverse proxy or CDN).

= 4.3.32 =
* Updated the plugin name and description to reflect OmniDirectory's broader positioning as an adaptable WordPress directory and listings plugin, rather than a staff-directory-only tool. No functional changes.
* Code quality: corrected the placement of several existing code-documentation comments (`phpcs:ignore`) so they correctly annotate the specific, already-reviewed, intentionally bounded queries used by the CSV import swap process. No query logic, behavior, or performance characteristics were changed.
* Added `omnidirectory_single_profile_extra_fields` extension point for companion plugins. No effect by itself.
* Added `omnidirectory_csv_import_aliases` extension point for companion plugins. No effect by itself.
* Added `omnidirectory_csv_row_meta_input` extension point for companion plugins. No effect by itself.
* Added `omnidirectory_profile_data_fields` extension point for companion plugins, allowing their own content to be printed inside the profile details area of all 8 single-profile layouts. No effect by itself.

= 4.3.31 =
* New: featured images from a CSV import are now downloaded and attached in the background, independently of member import and the directory swap — a slow or failing image can no longer affect whether the directory itself imports successfully.
* Security: background image downloads use WordPress's own safe HTTP request handling (rejecting unsafe destinations, including private/internal network addresses and their redirects) and a bounded maximum download size, instead of the plugin's previous unbounded image-fetching method.
* Reliability: a failed image download is retried a limited number of times, then left alone with its original URL and error kept for reference, without ever blocking the rest of the import.
* Reliability: background image processing is crash-safe — an interruption at any point, including right after an image file is saved, is recognized on the next attempt so the same image is never downloaded or attached twice.
* Import/Export page now shows a simple count of remaining and permanently failed images for the current import, separate from the member import status.

= 4.3.30 =
* Reliability: the existing directory is no longer touched while a CSV import is being staged in the background — new members are prepared privately first, and only swapped in to replace the previous directory once every new member is fully ready.
* New: the old-to-new directory swap itself runs as a resumable background process (via WP-Cron), publishing the newly staged members and retiring the previous ones in small batches rather than all at once.
* Reliability: the swap can safely resume after an interruption at any point — including partway through publishing new members or retiring old ones — without creating duplicates or affecting members outside the current import.
* Reliability: the same import lock with a renewing heartbeat used for staging now also protects the swap step, and "Resume Import" can continue a swap that appears to have stalled, not just the staging step.
* Performance: the live-search cache is invalidated once when a swap finishes, instead of separately for every member changed during the swap.
* Import/Export page wording updated to describe the new staged-then-swapped process instead of the previous immediate-replacement description.

= 4.3.29 =
* Reliability: CSV import now fully validates the entire file (row count, header, and every row's Name column) before touching any existing Directory Member — an invalid or oversized file is rejected up front with a clear message, with nothing changed.
* New: CSV import now runs as a resumable background job (via WP-Cron) instead of a single request, processing rows in small batches with a saved checkpoint after each one — removing the previous dependency on one long-running page request completing without a timeout.
* Reliability: importing members are staged privately in the background; each row is tracked so a batch that's interrupted and retried never creates duplicate members, and any categories left incomplete by an interruption are correctly finished on retry.
* Reliability: background import batches use a lease with a renewing heartbeat to coordinate safely, and a "Resume Import" action is available if a batch ever needs a manual nudge to continue.

= 4.3.28 =
* Performance: live search results are now cached, with an instant invalidation the moment any Directory Member is added, edited, trashed, restored, or deleted.
* Performance: the Category Filter dropdown no longer loads every member in a category to narrow results — it now queries only the already-matched search results directly.
* Security: live search now has basic rate limiting to reduce the impact of automated request flooding.

= 4.3.27 =
* Fix: a fatal error on member profile pages when a companion plugin added markup with a `display` CSS style — a callback was declared `private` but WordPress's hook system calls it from outside the class.

= 4.3.26 =
* Fix: the Polylang custom-field-sync filter now removes only this plugin's own fields, instead of unconditionally emptying the whole list (which silently disabled that Polylang feature for every other plugin's fields too).

= 4.3.25 =
* Fix: 4 settings (search placeholder, post type label, items per page, primary color) now correctly autoload, removing 4 unnecessary individual database queries per page load.

= 4.3.24 =
* Security: companion-plugin hook output (Action Bar, Interactive Map, QR Code, Rich Text Editor) is now escaped with `wp_kses()` instead of echoed raw.
* Security: CSV import now rejects malformed upload data before it can trigger a PHP error.

= 4.3.23 =
* Added a "Recent Activity" log on the Import/Export page, recording settings resets and CSV exports/imports.

= 4.3.22 =
* Removed the unused 'wysiwyg' meta-field mechanism entirely (was inactive on Free, per WordPress.org Trialware guidelines) and replaced it with two generic extension filters for companion plugins.
* Security: reduced unescaped-output exceptions from 52 to 2, with every remaining case individually justified.

= 4.3.21 =
* Added `omnidirectory_search_result_rendered` action for companion-plugin search-impression tracking. No effect by itself.

= 4.3.20 =
* Added Schema.org (JSON-LD) structured data to single member profile pages for better search engine understanding.

= 4.3.19 =
* Added `omnidirectory_settings_page_after` action for companion plugins to add their own settings sections. No effect by itself.

= 4.3.18 =
* CSV import now clears the existing directory (to Trash, recoverable) before importing, preventing duplicate entries on re-import.

= 4.3.17 =
* Moved the companion-plugin Action Bar extension point from search cards to the single profile page.

= 4.3.16 =
* Added `omnidirectory_result_card_actions` extension point for companion plugins. No effect by itself.

= 4.3.15 =
* Fixed a raw `<style>` tag on the Directory Settings page to use proper WordPress asset enqueuing, per WordPress.org review feedback.

= 4.3.14 =
* Updated "Tested up to" version per WordPress.org's requirements.

= 4.3.13 =
* Addressed WordPress.org review feedback: added reviewer to Contributors list; fixed 2 more unescaped-output findings.

= 4.3.12 =
* Fixed a Plugin Check false-positive warning on Bio field output (no functional change; output was already sanitized).

= 4.3.11 =
* Fixed the Rich Text Editor's text being invisible in dark-mode browsers.

= 4.3.10 =
* Fixed the Rich Text Editor's toolbar failing to load in some environments by explicitly enqueuing its assets.

= 4.3.9 =
* Diagnostic build (superseded by 4.3.10).

= 4.3.8 =
* Simplified the Bio field's Rich Text Editor rendering; removed a redundant asset-loading call.

= 4.3.7 =
* Fixed the Rich Text Editor's toolbar reverting to defaults on page reload.

= 4.3.6 =
* Removed the unused default content editor from the Directory Member edit screen.

= 4.3.5 =
* Fixed a translated member profile 404 issue on multilingual (Polylang) sites.

= 4.3.4 =
* Adjusted the Rich Text Editor's internal ID format for better compatibility.

= 4.3.3 =
* Directory Member edit screens now always use the classic editor, for Rich Text Editor reliability. Filterable.

= 4.3.2 =
* Fixed the Rich Text Editor's toolbar not loading its full configuration under the Block Editor.

= 4.3.1 =
* Minor compatibility and stability fixes.

= 4.1.0 =
* Fixed several PHPCS code-standards findings in the CSV export/import handlers, with no behavior change.

= 4.0.0 =
* Replaced the Sample Data Generator with a full CSV Import/Export module, including automatic column matching and image fetching.

= 3.10.0 =
* Added 3 more optional custom fields (Field 3, 4, 5), each with a customizable label.

= 3.9.0 =
* Rebuilt the single-profile Language Switcher on buttons instead of links, for reliability.

= 3.8.1 =
* Resolved 9 PHPCS code-standards findings, with no behavior change to search or multilingual support.

= 3.8.0 =
* The Language Switcher now swaps profile content via AJAX in place, without a full page reload.

= 3.7.1 =
* Hardened the multilingual (Polylang/WPML) integration against partial or broken installs.

= 3.7.0 =
* Added an automatic Single Profile Language Switcher for Polylang/WPML sites.

= 3.6.0 =
* Explicit Polylang/WPML integration improvements for post type and category visibility.

= 3.5.1 =
* Fixed Arabic search results being excluded on language-mismatched pages.

= 3.5.0 =
* Fixed multilingual (Polylang/WPML) search to correctly stay scoped to the visitor's language.

= 3.4.3 =
* Resolved the final Plugin Check warning.

= 3.4.2 =
* Resolved 4 more Plugin Check warnings on a search query.

== Upgrade Notice (older versions) ==

= 3.4.0 =
Recommended update: resolves all WordPress Plugin Check findings (heredoc usage, header fields, escaping, sanitization, and SQL preparation) with no feature changes.
