fix(instanda): hygiene pass + v1.1.0 (uninstall, notices, cron, resubmit, theme)

- F-15: uninstall now removes ALL espinstanda_* options and transients (incl. the
  encrypted password and its self-provisioned key).
- F-16: register the "Gravity Forms required" admin notice unconditionally so it is
  reachable when GF is inactive (gform_loaded never fires then).
- F-22: self-heal the daily retention cron on admin_init (matches the schema self-heal).
- F-19: re-validate the stored payload via Field_Mapper::validate_payload() before a
  resubmit (DB-tamper trust boundary).
- F-20: increment the retry count on resubmit and dispatch the previously-dead
  espinstanda_resubmit_success notification event.
- F-23: emit the theme's Form-5 date-range JS only on pages that embed a Gravity Form
  and disconnect the MutationObserver (was observing the DOM subtree forever).
- F-24: bump plugin to 1.1.0; add readme.txt changelog documenting the audit fixes and
  the required ops actions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 16:20:10 -06:00
co-authored by Claude Fable 5
parent 0732dd662d
commit 5d2e9cb7e5
7 changed files with 175 additions and 18 deletions
@@ -121,6 +121,37 @@ final class Field_Mapper {
return $errors;
}
/**
* Sanity-check a stored StartQuote wire body before re-sending it (resubmit trust
* boundary): the DB is treated as untrusted input. Returns error codes (empty = ok).
* Pure - uses a fixed UTC zone since only Y-m-d well-formedness is being checked.
*
* @param array<string,mixed> $body A body previously built by to_payload().
* @return list<string>
*/
public function validate_payload(array $body): array {
$errors = [];
foreach (['Role', 'ActivityType', 'PQ_LocationState_CHOICE'] as $key) {
if (trim((string) ($body[$key] ?? '')) === '') {
$errors[] = $key . '_missing';
}
}
if (!$this->is_positive_integer($body['PQ_MaximumDailyAttendance_NUM'] ?? null)) {
$errors[] = 'attendance_invalid';
}
$email = trim((string) ($body['PQ_CustomerEmailAddress_TXT'] ?? ''));
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'email_invalid';
}
$tz = new \DateTimeZone('UTC');
foreach (['PQ_EventStart_DATE', 'PQ_EventEnd_DATE'] as $key) {
if ($this->parse_date((string) ($body[$key] ?? ''), $tz) === null) {
$errors[] = $key . '_invalid';
}
}
return $errors;
}
/** D8 - inclusive consecutive-day count: (end - start) + 1; a single-day event is 1. */
private function consecutive_days(\DateTimeImmutable $start, \DateTimeImmutable $end): int {
return (int) $start->diff($end)->days + 1;
@@ -18,7 +18,7 @@ namespace ESP\Instanda;
final class Instanda_AddOn extends \GFFeedAddOn {
protected $_version = '1.0.7';
protected $_version = '1.1.0';
protected $_min_gravityforms_version = '2.10.3';
protected $_slug = 'espinstanda';
protected $_path = 'esp-instanda-integration/esp-instanda-integration.php';
@@ -159,14 +159,24 @@ final class Resubmit_Handler {
$store->append_event($tx_id, 'Resubmit aborted - stored payload unreadable.', 'error');
return;
}
// Trust boundary: the stored body is untrusted input on the way back out to the
// insurer - re-validate it before re-sending (guards against DB tampering).
$payload_errors = (new Field_Mapper())->validate_payload($body);
if ($payload_errors) {
$store->append_event($tx_id, 'Resubmit aborted - stored payload failed re-validation: ' . implode(', ', $payload_errors), 'error');
return;
}
$store->increment_retry($tx_id); // this attempt is a retry - keep the count truthful
$result = (new Api_Client())->start_quote($body, $tx->dedupe_key);
if ($result->ok && $result->quote_ref !== null) {
if ($result->ok && $result->quote_ref !== null && $result->quote_ref !== '') {
$store->mark_success($tx_id, $result->quote_ref, $result->url_single_use, $result->http_status);
Failure_Surface::bump();
if ($tx->entry_id) {
Entry_Integration::record($tx->entry_id, $result->quote_ref, (string) $result->url_single_use, 'delivered', $tx->environment);
self::notify_resubmit_success((int) $tx->entry_id);
} else {
// Stranded lead (no entry) - mint a fresh link and email the customer (option B).
$fresh = (string) $result->url_single_use;
@@ -182,4 +192,19 @@ final class Resubmit_Handler {
Failure_Surface::bump();
}
}
/** Dispatch the registered espinstanda_resubmit_success notification event for an entry. */
private static function notify_resubmit_success(int $entry_id): void {
if (!class_exists('\GFAPI')) {
return;
}
$entry = \GFAPI::get_entry($entry_id);
if (is_wp_error($entry)) {
return;
}
$form = \GFAPI::get_form((int) $entry['form_id']);
if ($form) {
\GFAPI::send_notifications($form, $entry, 'espinstanda_resubmit_success');
}
}
}