=== Cordex Forms ===
Contributors: gerhardpretorius
Tags: dataverse, dynamics 365, crm, forms, power platform
Requires at least: 6.2
Tested up to: 7.0
Requires PHP: 7.4
Stable tag: 1.8.41
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Build submission forms against Microsoft Dataverse and Dynamics 365. Every submission creates a record in your own environment.

== Description ==

Cordex Forms connects your WordPress site to Microsoft Dataverse, the data platform behind Dynamics 365 Customer Engagement and Power Apps. You build a submission form in the admin, place its shortcode on a page, and each submission is written to your environment as a new record.

The connection uses a service principal (a Microsoft Entra app registration added to your environment as an application user), so it does not depend on a signed-in person.

**What the plugin does**

* Connects to one Microsoft Dataverse / Dynamics 365 environment using a service principal. You can test the connection from the admin; the result and its timestamp are saved.
* Builds submission forms in the admin: choose a table, tick the columns you want by display name, and place the generated `[cordex_form id="..."]` shortcode on any page. There is no limit on the number of forms, and no other plugin is required.
* Supports these Dataverse column types: single-line text, multi-line text, single choice (option set), yes/no, whole number, decimal, float, currency, and date/time. Choice and yes/no options are read from your table's own metadata. Date/time values are sent in the representation the column's behaviour requires (UTC for User Local, a bare date for Date Only, wall-clock for Time Zone Independent).
* Validates every submission server-side against the table's metadata before a record is created. The browser only ever posts values against a saved form id, never field definitions.
* Turns on anti-abuse by default: a nonce, a honeypot field, a minimum fill-time check, and per-IP rate limiting on the submission endpoint (10 submissions per 10 minutes by default).
* Queues submissions when Dataverse is unreachable. Accepted submissions are stored locally, encrypted at rest, and retried automatically with exponential backoff and jitter, honouring any Retry-After the service sends. The visitor still sees the normal success message. Items that cannot succeed are parked on the Queue screen for review, retry, or discard, and the site administrator is emailed (at most once every six hours).
* Records consent: an optional required consent checkbox with your own wording. Each edit of the wording appends a new immutable version to the form's history, and queued submissions record the version and UTC timestamp they were submitted under.
* Captures submission context server-side: landing page, referrer and browser user agent, plus the visitor's IP address stored full, truncated, hashed, or not at all, as you choose. UTM parameters and Google/Meta/Microsoft click ids are captured only when the visitor consents. Each detail can be written to a text column on the record or kept local only.
* Lays out forms in one to three columns of titled sections, with optional column widths, button and colour theming, twelve presets, and themes you can save and reuse. Unset options inherit your active WordPress theme.
* Encrypts the client secret at rest in your database using the PHP sodium extension. The secret is write-only: once saved it is never rendered or returned by any screen or endpoint.
* Guards the environment URL against SSRF: only Microsoft-operated Dataverse and Dynamics hosts are accepted, checked both when saved and again before every outbound request. Redirects are never followed.
* Integrates with the WordPress privacy tools: the personal-data exporter and eraser cover the plugin's queued submissions.
* Exports and imports the whole configuration as a JSON file for backup or migration. Client secrets are never written to an export.
* Uninstalls cleanly, removing the plugin's options, table, scheduled event and cached tokens, with an opt-in setting to retain data for a migration.

**Requirements**

* WordPress 6.2 or later and PHP 7.4 or later.
* The PHP sodium extension (`sodium_crypto_secretbox`). It is required, not optional: the plugin encrypts credentials with it and will not start without it. Sodium is bundled with PHP 7.2 and later on most hosts.
* Your own Microsoft Dataverse / Dynamics 365 environment, and an application user (service principal) with privileges to create rows on the tables you write to. Cordex Forms does not provide the Microsoft environment.

**Cordex Forms Pro**

Everything described in this readme is included in this plugin and is fully functional as installed. Nothing here is limited, timed, restricted by quota, or unlocked by a key, and the plugin contains no licensing code of any kind. Cordex Forms Pro is a separate plugin, installed alongside this one, for sites whose tables need column types and features this plugin does not implement:

* Lookup, Customer, Owner and party-list columns, with live record search.
* File and image upload columns, and multi-select choice columns.
* Related-record writes, and submitting in a specific Dataverse user's security context.
* Reading Dataverse data back out to the site: data grids and record pages.
* More than one environment, and an overview of which component targets which environment.

Details: https://myitandapps.com.au/products/cordex-forms/ - support: cordex-forms@myitandapps.com.au

**External services**

This plugin contacts two remote endpoints, both of them Microsoft services that you configure. It does not contact the plugin author or any other third party.

*1. Microsoft Entra ID - login.microsoftonline.com*

Used to obtain the OAuth 2.0 access token that authorises every call to your Dataverse environment.

* What is sent: an HTTP POST to `https://login.microsoftonline.com/{tenant id}/oauth2/v2.0/token`, with your Microsoft Entra tenant id in the URL path and, in the request body, `grant_type=client_credentials`, the application (client) id, the client secret, and a scope of `{your environment URL}/.default`. These are the credentials you entered on the Environments screen. No visitor data and no form data are ever sent to this endpoint.
* When it happens: only when the site does not already hold a valid cached token. The token is cached in a WordPress transient for the lifetime Microsoft reports, less sixty seconds - typically about one hour - so under continuous use the endpoint is called roughly once an hour. It is also called when you use Test connection.
* Service terms: https://www.microsoft.com/licensing/terms/
* Privacy statement: https://privacy.microsoft.com/privacystatement

*2. Your own Microsoft Dataverse / Dynamics 365 environment*

This is the environment URL you enter on the Environments screen, for example `https://yourorg.crm6.dynamics.com`. It is your own Microsoft tenant, under your own agreement with Microsoft. It is not a service operated by the author of this plugin, and the author has no access to it.

* What is read: table, column and choice metadata from the Web API (`EntityDefinitions`, `Attributes`, option sets), plus a `WhoAmI` call when you use Test connection. Metadata responses are cached locally for about ten minutes.
* What is written: one record per submission, created with `POST /api/data/v9.2/{entity set}`. The record contains the values the visitor entered in the form's fields, and - only for the details you have explicitly mapped to a column on the Shortcodes screen - the landing page URL, the HTTP referrer, the browser user agent, the visitor's IP address in the form your IP mode produces (full, truncated, hashed, or omitted entirely), and, when the visitor has ticked the consent box, the `utm_source`, `utm_medium`, `utm_campaign`, `utm_term` and `utm_content` parameters and the `gclid`, `fbclid` and `msclkid` click ids. A per-submission correlation reference and the consent version are also written where you have mapped columns for them. Details you have not mapped are never sent to Dataverse.
* When it happens: on every form submission; when an administrator opens the form builder or saves a form, to read metadata (subject to the ten-minute cache); when you use Test connection; and on an unattended scheduled task. That scheduled task is a WordPress cron event that runs every five minutes, claims up to ten queued submissions that are due, and attempts to deliver them to your environment. It is registered when the plugin is activated and removed when it is deactivated, and it will make outbound requests without an administrator being present whenever the queue is not empty.
* Terms and privacy for this endpoint are those of your own Microsoft agreement; see https://www.microsoft.com/licensing/terms/ and https://privacy.microsoft.com/privacystatement

No data is sent to the plugin author or to any third party. The plugin performs no analytics, telemetry, usage tracking, update checks or remote logging of any kind.

**Bundled fonts**

The plugin's admin screens use two open-source typefaces, both bundled with the plugin and served from your own site. No CDN or external font service is contacted.

* Inter, copyright the Inter Project Authors, SIL Open Font License 1.1.
* Plus Jakarta Sans, copyright the Plus Jakarta Sans Project Authors, SIL Open Font License 1.1.

The full licence text for each is included in `assets/fonts/` as `ofl-inter.txt` and `ofl-plusjakartasans.txt`. The font files are used only in wp-admin; the front-end form ships no stylesheet and inherits your active theme.

**Trademarks and affiliation**

Cordex Forms is an independent product developed by My IT & Apps Pty Ltd. It is not affiliated with, endorsed by, or sponsored by Microsoft Corporation. "Microsoft", "Microsoft Entra", "Microsoft Dataverse", "Dynamics 365", "Power Apps" and "Power Platform" are trademarks of the Microsoft group of companies. "WordPress" is a trademark of the WordPress Foundation. These names are used only to describe compatibility.

Licensing, updates and checkout for this plugin are powered by [Key-Warden](https://key-warden.com/?ref=wp-cordex-forms).

== Installation ==

1. Upload the `cordex-forms` folder to `/wp-content/plugins/`, or install the plugin through the Plugins screen in WordPress.
2. Activate the plugin through the Plugins screen.
3. Create a Microsoft Entra app registration with a client secret, and add it to your environment as an application user with privileges to create rows. See https://learn.microsoft.com/power-platform/admin/manage-application-users
4. In WordPress, go to Cordex Forms - Environments, enter the environment URL, tenant id, application (client) id and client secret, then use Test connection.
5. Go to Cordex Forms - Shortcodes, build a form by choosing a table and ticking its columns, and place the generated `[cordex_form id="..."]` shortcode on any page.

== Frequently Asked Questions ==

= Does this require another forms plugin? =

No. Cordex Forms includes its own submission form. You build it in the admin, place it with the `[cordex_form]` shortcode, and each submission creates a Dataverse record.

= Do I need anything from Microsoft? =

Yes. You need your own Microsoft Dataverse or Dynamics 365 environment and an application user (service principal) that can create rows on the tables you want to use. Cordex Forms does not provide the Microsoft environment, and cannot work without one.

= Why does the plugin say it needs the PHP sodium extension? =

Because it encrypts your Dataverse client secret, and every queued submission, with libsodium's authenticated encryption. If `sodium_crypto_secretbox()` is unavailable, the plugin refuses to start and shows an admin notice rather than storing credentials unprotected. Sodium ships with PHP 7.2 and later and is enabled by default on most hosts; if yours has it disabled, ask your host to enable it.

= Which Dataverse column types can a form use? =

Single-line text, multi-line text, single choice (option set), yes/no, whole number, decimal, float, currency, and date/time. Column types this plugin does not implement are simply not offered in the field picker. If a table marks such a column as required, the form builder will refuse to save the form and explain why, because Dataverse would reject every submission.

= How many forms can I build? =

As many as you like. Each form is built in the admin and placed with its own shortcode.

= What data leaves my site? =

Only what goes to Microsoft: an authentication request to login.microsoftonline.com, and reads and writes against the Dataverse environment URL you configured. Nothing is sent to the plugin author or to any third party. See the External services section above for the full detail.

= Where are my credentials stored? =

The environment client secret is encrypted at rest in your WordPress database with the PHP sodium extension. The encryption key comes from the `CORDEX_ENCRYPTION_KEY` constant when you define one, otherwise it is derived from your WordPress salts. The plaintext secret is decrypted server-side only to authenticate to Dataverse, and is never returned to the browser by any screen or endpoint.

= What happens if Dataverse is down when someone submits? =

The submission is accepted, stored encrypted in a local queue, and retried automatically with exponential backoff (honouring any Retry-After the service sends) by a scheduled task that runs every five minutes. The visitor sees the normal success message throughout. If an item still cannot be delivered, it is parked on the Queue screen and the site administrator is emailed.

= Is there a paid version? =

Yes. Cordex Forms Pro is a separate plugin, installed alongside this one. It adds column types and features this plugin does not implement - lookup, customer, owner, party-list, multi-select, file and image columns, related-record writes, reading Dataverse data back out to the site, and more than one environment. Everything described in this readme works fully without it, and nothing here is disabled or restricted when it is absent. See https://myitandapps.com.au/products/cordex-forms/

= Does uninstalling remove everything? =

Yes. Uninstalling removes every option, table, transient and scheduled task the plugin created, including the stored environment and its encrypted secret, the saved forms and shortcodes, and the delivery queue. Back up first if you need to: the Environments screen exports the configuration as JSON, though client secrets are never included in an export. An opt-in setting preserves the data through an uninstall if you are migrating.

= Is Cordex Forms affiliated with Microsoft? =

No. It is an independent product from My IT & Apps Pty Ltd. See the trademark note in the description.

== Changelog ==

Entries describe this plugin. Where a fix also applied to the separate Cordex Forms Pro add-on, that is noted; nothing here requires the add-on.

= 1.8.41 =
* The "Enable auto-updates" control no longer disappears from the plugin row after an update. WordPress offers that control only for a plugin listed in the update transient's response or no_update list - WP_Plugins_List_Table sets 'update-supported' from precisely that - and core fills those lists from the Update URI filter only AFTER it contacts the API, returning early while its timeout window is open. Installing an update then deletes the transient in wp_clean_plugins_cache(), which runs AFTER upgrader_process_complete, so the update hook cannot usefully write the entry either. Between those two facts sat a window, after every manual update, where the plugin was in neither list and the control silently vanished. It healed itself on the next unthrottled check, which is exactly what made it read as a lost setting rather than a timing artefact. The plugin is now guaranteed to be listed whenever the transient is READ - no network call, no write, no dependence on when core last checked.
* Strictly additive, and tested as such. A pending update under response is left exactly as core wrote it, and so is an existing no_update entry; only a genuine absence is filled, with the installed version and an empty package. Overwriting a real pending update with "up to date" would hide an available release, which would be a considerably worse fault than the one being fixed.
* The test asserts the filter is REGISTERED as well as correct. The four behaviour cases call the method directly and would all still pass with it never hooked up - a right answer nobody asks for. Removing the registration now fails the suite.

= 1.8.40 =
* NO VALIDATION ON EMAIL, PHONE AND WEB ADDRESS on forms built before that validation existed - and the validator was never the problem. A form definition is a SNAPSHOT: the server resolves every field from live metadata at save time and persists it, so a public form costs no metadata call per visitor. The price is that a form saved before a field gained a property has a definition without it, and nothing ever goes back for it. Format-aware validation reads the field's String Format (Email / Url / Phone), so every form built before that property existed handed the check an empty format, the check concluded "plain text", and it accepted anything. Required kept working, because required WAS in the snapshot - which is why the form refused an empty Topic and in the same breath accepted "fhjfhfhh" as an email address and "fghjfghfghjfghfghf" as a website. Re-saving the form in the builder would have fixed it; nobody should have to know that. A stored definition now has its missing formats filled in from live metadata as it is read.
* The repair is applied in ONE place - on the way out of the definition getter - because all four consumers go through it: this plugin's renderer and submit handler, and the add-on's two. The same property has now gone missing in three separate places, each time because a second copy of a read did not know about it, and four call sites would have been four chances to miss the fourth. A definition that is already complete costs nothing, and a metadata read that fails leaves the definition exactly as it was rather than weakening the form.
* A DISABLED submit button now looks and behaves disabled. Holding it closed until required consent is given was only half the job: it kept its full-strength fill and still brightened under the pointer, so a button that could not be used was indistinguishable from one that could - reported as it appearing active, with a hover effect, while the consent switch was off. The rule is emitted whatever the button style setting says; the previous treatment lived inside the branch that only runs when a style is chosen, so a themed button had no disabled state at all.
* The non-production banner has space above it as well as below. It carried a bottom margin only, which is fine when it opens a component - but a page with more than one component put the banner directly beneath the previous one's message bar, flush against it, the two reading as a single block.
* The fixture that measures the button's appearance uses the stylesheet the plugin actually emits, not a copy of the rule under test written into the fixture. The first version inlined the expected CSS by hand, which would have passed whether or not the plugin emitted anything at all - a test that reports a fix as present while the shipped stylesheet is empty is worse than no test. It also no longer borrows another screen's scoped CSS as a fallback: those rules are scoped to a form's dom id and match nothing on a different form, which reported a styled control as unstyled.

= 1.8.39 =
* THE SUBMIT BUTTON STOPPED WORKING AFTER THE FIRST SUCCESSFUL SUBMISSION, and stayed that way. The button is disabled the moment a submit starts, so a request cannot be fired twice. It was re-enabled when the server refused, and re-enabled when the network failed - but not when the submission SUCCEEDED. So the first submission that worked left the button disabled for good: the record was created, the form reset itself, and from then on every click did nothing at all. No event, no reload, no message, no request. Editing the fields afterwards cleared the success notice as well (that is what the notice is meant to do), removing the last trace that anything had happened - so the page looked untouched and the button looked alive. Reported as "I click button and nothing" and, entirely reasonably, as validation being broken: nothing was reaching the server to be validated a second time. The button is now restored as the FIRST statement of the response handler, ahead of every branch, which is how the record page has always done it and why its Save button never stuck.
* That rule is now enforced across both plugins rather than fixed in one place. The disable-around-a-request pattern appears in five call sites across the two add-ons, and it has exactly one failure mode: a re-enable that sits inside a branch is a re-enable that some outcome skips. tools/button-restore-check.js walks every one of them and requires the restore to come before any branch. Where it cannot walk a promise chain to the end it says so by name rather than reporting a pass over ground it never covered.
* Nothing caught this because every fixture in the repository submitted ONCE. A one-shot test cannot see a control that only breaks after it has been used, and "it works the first time" is the most misleading kind of green there is. The harness now submits, succeeds, and submits again.
* The submit button is held closed until required consent is given, and opens the moment it is ticked. A required consent switch that is off is the one thing on the form that can stop a submission while every visible field looks correct; leaving the button live meant the only feedback was a round trip and a sentence at the bottom of the page. Checked in both directions, because a button that is always disabled would pass a one-sided test and be a worse fault than the one it replaces.
* The message bar is scrolled into view when it is shown, and only when it is actually out of view. On a long form the button is the last thing on the screen and the bar renders past the bottom edge, so an answer could be delivered to a place nobody was looking. This was NOT the cause of the dead button above - it was found while looking for it - but it is a real way for a reply to go unseen.
* Buttons follow their own text size. The button was a fixed 32px box with 14px of side padding and line-height 1 - correct at 14px type and wrong at every other size, because nothing about the box moved when the type did. Raise the type and the words crowd the edges and the descenders run at the bottom rule. Padding and the height floor are expressed in em now, so a button keeps its proportions at any size; the numbers are the old ones divided by 14, so a button at the default size is pixel-identical to before.
* The consent switch can be reached with the keyboard. Turning the checkbox into a switch hid the real input at zero width and zero height, which takes it out of the tab order and leaves a browser unable to point a validation message at it. It now carries the switch's own 44x24 footprint at zero opacity: invisible, still focusable.

= 1.8.38 =
* Fixed, from 1.8.37: the designer's numbered steps rendered as a tall stack of full-width rows instead of a row of tabs across the top.

= 1.8.37 =
* Email, phone and website fields are now checked - as you type, and again on the server. Dataverse records what a text column is FOR in its format, and the plugin was not reading it, so nonsense went into a phone or an email column and was accepted. Both are checked now, and a form's inputs use the right keyboard on a phone.
* The check is deliberately not strict about style: "+61 (2) 9876 5432", "555-0158 ext 12" and a bare "example.com" are all accepted. It rejects what cannot be a number or an address, not what is written unusually.
* Text length limits from Dataverse are applied to single-line text columns, which had been read as unlimited.
* Fixed, from 1.8.36: the designer's step navigation scrolled sideways instead of wrapping, and did not go back to a vertical list on a narrower window.
* Fixed, from 1.8.36: the line under the Shortcodes header stopped halfway across its card.

* The Shortcodes screen is balanced: on a wide monitor the designer now has a margin on its right matching the menu on its left, instead of running to the edge of the window. On narrower screens the full width still goes to the designer.
= 1.8.36 =
* The form designer now uses the width of your screen. The layout designer rendered at the same 620px whether your monitor was 1600px or 2560px wide, which left about 48px for each section's title - too narrow to read what you had typed. It now takes roughly two-thirds to four-fifths of the window, and a section title box is between 220px and 533px depending on your screen.
* A section card's title box drops the buttons beside it onto their own line when the column is narrow, instead of being squeezed by them.
* Reading widths are unchanged. Prose, cards and text inputs still stop at a comfortable measure rather than following the monitor - only the working surfaces you arrange things on have been widened.

= 1.8.35 =
* Fixed: with Cordex Forms Pro installed, a published form was capped at 980px and its columns squeezed into part of the width the page gave it. Without the add-on the same form was full width. Admin styling was reaching published pages on add-on sites; it no longer does, and the form takes the width your theme gives it either way.
* Security: cached Microsoft access tokens are now encrypted in the database. Your client secret was always encrypted; the token minted from it was not, so anyone reading the database had a usable credential for up to an hour. Reported by an external security review.
* Fixed: changing a client secret left the old access token in the database until it expired. The flush that was meant to remove it had been deleting the wrong entry, and never removed anything.
* A choice column whose options cannot be read is now refused when you save the form, and a submission against one is refused with a clear reason, instead of being sent to Dataverse to fail there.
* The "sodium extension required" notice is shown only to users who can actually install one.
* Uninstall removes one further option that could previously survive on a fallback path.

= 1.8.34 =
* Fixed: saving a form failed with "Unknown field type." against the text-size rows added in 1.8.33. The control rendered and accepted a value, and the server then refused it. Anyone on 1.8.33 should update.
* The consent control now follows the form's own Yes/No setting: a form whose two-option fields render as Dataverse switches gets a switch on its consent row instead of a lone square checkbox. What is submitted does not change in either style.
* The consent control is centred on its wording rather than pinned to the first line of it, which matters because a consent sentence usually wraps.
* Fixed 35 accessibility warnings on the form designer: every control now carries an id, and each row's label is a real label element wired to its control, so label text is clickable and screen readers no longer announce the screen as unlabelled inputs.
* Export and import now carry a form's rendered appearance, not only the values typed into the designer. Those are two separate records and only the second decides what a visitor sees.

= 1.8.33 =
* New: label text size and field text size in the form designer, as a number and a unit (px, %, em, rem, pt, vw, vh). Both blank by default, meaning inherit your theme. Both travel with a saved style theme.
* Fixed: the consent checkbox rendered at the browser default size and its wrapping sentence ran back underneath it.

= 1.8.32 =
* Fixed a silent data-loss path: a number such as 1e999 passed validation, became infinity, and could not be encoded - so the submission was refused while the visitor was still shown the success message, and the recorded error named the wrong cause. Non-finite numbers are now refused at the field, where they can still be corrected.
* Fixed: deleting a saved shortcode reloaded the screen whether or not the delete had succeeded, so an expired nonce looked exactly like success until the row came back.
* Admin screens no longer hang silently. A server error, a proxy error page or an empty response left screens on "Saving..." indefinitely; the status and body are now always carried through, so "the server said no" is distinguishable from "the server never answered".
* The yes/no switch has a visible keyboard focus ring (WCAG 2.4.7). The real checkbox is transparent so the switch can be drawn, which had hidden the browser's own focus ring with it.

= 1.8.31 =
* Fixed: the saved-shortcodes card was far wider than the cards beside it. Admin width is now structural rather than per-element, so anything added later inherits a sensible width instead of needing one remembered for it.

= 1.8.30 =
* The form designer takes a wider working width - a four-column field picker, a three-column layout designer and a table of metadata targets are a working surface, not prose.
* The preview has its own titled card, hidden until there is something in it, instead of floating loose beneath the other cards.
* Fixed: the preview explanation printed twice on the same screen.

Earlier releases predate this plugin's submission to the WordPress Plugin Directory. The full history ships with the plugin.

== Upgrade Notice ==

= 1.8.38 =
Fixes the designer's step navigation, which rendered as a tall stack of rows in 1.8.37.

= 1.8.37 =
Email, phone and website fields are now validated - invalid values were previously accepted and written to Dataverse.

= 1.8.36 =
The form designer now uses the width of your screen - section titles were previously squeezed to about 48px.

= 1.8.35 =
Encrypts cached Microsoft access tokens in the database and fixes a token flush that never ran. Recommended for every site.

= 1.8.34 =
Fixes a blocker in 1.8.33: forms using the new text-size options could not be saved. Update if you are on 1.8.33.
