Skip to content

v6.14.0

Choose a tag to compare

@github-actions github-actions released this 19 Jul 23:19
· 15 commits to main since this release
dba61ae

Added

  • Settings → General (#711) — a "Module settings" index card that links straight to the settings surfaces some modules keep next to themselves (Scheduling settings, Recruitment settings). Each link is shown only to users who can open that module's settings, so it improves discoverability of configuration split across the plugin without moving anything.
  • Documentation tab (#674, #697) — greatly expanded the reference, with new or rewritten pages across every functional area: the form builder and per-form settings (dynamic fields, open/close schedule, geofence, per-form email, security/anti-fraud, public operator access), Scheduling (personal calendars and audience calendars), Reregistration campaigns, Recruitment, Short URLs, the Developer Hooks/REST/Forms API, a live-generated Capabilities & Roles reference, a User Dashboard & Access page, and Emails & Delivery — plus an in-page search box and feature-grounded Troubleshooting rows. Every area was verified against the code.
  • Settings → SMTP → Email Model (#674) — a "Send a test email" button that sends one test message through the shared email pipeline (Email Model chrome + transport) so operators can confirm delivery is working. The message always goes to the current user's own account email (never a request-supplied address); it requires the settings-manage capability plus a nonce, and reports the outcome (sent / globally disabled / no address on file / send failed) as a notice on the SMTP tab.
  • Settings → SMTP (#674) — a recommendation box for the sibling total-mail-queue plugin, shown only when it is not active (via Integrations\MailQueue::is_active()), pointing operators to queued delivery with automatic retries + true multipart for reliable and bulk sending. Hides itself once the plugin is installed.
  • Central detection of the sibling total-mail-queue plugin (Integrations\MailQueue::is_active()), matching on the plugin folder across per-site and network activations and overridable via the new ffcertificate_mail_queue_active filter. Every plugin email already funnels through wp_mail(), so a mail-queue plugin captures them all for queueing/retry for free — the plugin deliberately ships no queue of its own; this single detector backs that decision and the (upcoming) "install total-mail-queue" recommendation shown only when it is absent. (#673)
  • New ffcertificate_email last-mile filter at the transport chokepoint (Core\EmailService::send()): a single place for integrations to inspect or rewrite the fully-composed message — to, subject, body, headers, attachments — just before wp_mail(), instead of hooking wp_mail globally and pattern-matching the plugin's mail. Fires after the global disable toggle (a disabled send never reaches it) and before the text/plain alternative is derived, so a rewritten body also drives the derived plain text. (#673)
  • Every HTML email the plugin sends now goes out as multipart/alternative (the HTML plus an auto-derived text/plain part), so mail renders in text-only clients and reads better to spam filters. The plain-text part is built once, centrally, at the transport chokepoint (Core\EmailService::send()) from the already-composed HTML — it works on WordPress core with no SMTP/queue plugin required — and can be customized or turned off per message via the new ffcertificate_email_plain_text filter (return an empty string to send HTML-only). (#673)
  • A configurable "Email Model" (Settings → SMTP) that styles the single chrome shared by every plugin email — header band (logo or site name, colors, alignment, padding), body card (colors, font, size, padding, width), footer (colors + tokenized text with {{site_title}} / {{recipient}} / {{year}} …) and outer wrapper (color, radius, padding) — with a live preview and a restore-to-defaults button. Backed by a dedicated ffc_email_template option (Core\EmailTemplateOptions) and a rewritten table-based, inline-styled templates/emails/layout.php (Gmail/Outlook-safe); the self-scheduling appointment emails now render their inner content through this shared chrome. Part of the plugin-wide email consolidation. (#662)
  • A soft "emails are turned off" heads-up now appears at the top of every email-editing surface (form Email tab, recruitment settings, self-scheduling and audience calendar editors, the reregistration campaign editor, the SMTP tab) whenever the global disable toggle is on, so a disabled switch no longer silently swallows mail while an operator edits a template. First step of the plugin-wide email consolidation. (#662)
  • Audience calendars can now notify an admin on new bookings and cancellations — two per-schedule opt-in toggles (default off) plus a recipient field (comma-separated; empty falls back to the site admin email), in the calendar editor's Notifications section. Mirrors the self-scheduling calendars, which already had admin notifications. The admin notification is independent of the existing per-schedule user toggles, so an admin can be alerted even when end users are not (or when the audience is empty). Adds notify_admin_on_booking / notify_admin_on_cancellation / admin_notification_emails columns to the schedules table via an idempotent migration. (#661)
  • Form editor → Email tab: a "Restore Default Text" button that repopulates the message editor with the default template (after a confirm), for when an operator has edited the body and wants the default back. The helper text also notes that simply clearing the body falls back to the default template when the email is sent. (#660)
  • Per-form "Notify Admin on Submission" opt-in (default off) with an optional recipient list, in the form's Email tab. Because re-wiring the dispatch above also revives the admin notification — which previously defaulted to the site admin email with no toggle — it is now gated behind this explicit opt-in so no admin is emailed on every submission without consent. (#649)
  • Device-fingerprint limit, "global on / form off" gap: the form editor now shows a neutral nudge when the subsystem is enabled plugin-wide but off for the form (explaining the shared-device trade-off so operators enable it deliberately), and forms in that state render a generic "submissions are logged and may be audited for fraud prevention" line inside the existing LGPD consent block. The line is intentionally generic — no device signal is collected when the form limit is off, so it makes no device-duplicate claim; the honest device disclosure still appears only when the per-form limit is on. (#647)

Changed

  • Documentation tab (#674, #697) — reorganized the tab by functional area: the Quick Navigation is now a tree mirroring the plugin's admin menus (Overview · Certificates & Forms · Scheduling · Reregistration · Recruitment · Short URLs · Developer · Troubleshooting), driven by a single recursive registry so the nav and render order stay in sync, and using WordPress dashicons instead of emoji. The Overview page was rewritten as a concise plugin-wide introduction. Also removed six dead legacy emoji-icon CSS rules left after the dashicons migration (a wider CSS audit kept the rest, which are generated in PHP or come from libraries such as CodeMirror and WP list tables).
  • Internal (#673) — the certificate email-body editor's "Restore Default Text" button now uses the same shared assets/js/ffc-email-restore-default.js as the recruitment and self-scheduling editors (the generic data-editor / data-default-key button), and the bespoke ffc-form-editor-email-metabox.js was retired — one restore-button implementation plugin-wide. Behavior-preserving.
  • Internal (#662) — the three editable default email bodies (certificate, recruitment convocation, self-scheduling confirmation) moved from inline PHP methods to templates/emails/{certificate-user,recruitment-convocation,selfscheduling-confirmation}.php (loaded via Core\EmailTemplates), so every default email body is now a file. Also renamed the internal term "miolo" to "email body" throughout the code/comments, and documented the one-pipeline email architecture in CLAUDE.md. Behavior-preserving.
  • Internal (#662) — the emails that still bypassed the shared chrome now go through it too, fully satisfying "every email → one configurable chrome": the submission admin notification, the self-scheduling admin notification, the capability-grant / access-granted email, and the "calendar deleted → appointment cancelled" notification (the last two converted from plain text to branded HTML). Their inner content moved to templates/emails/{submission-admin-notification,access-granted,calendar-deleted-cancellation}.php (the self-scheduling admin body was already a file).
  • Certificate submitter email (#662) — the editable body is now the "email body" wrapped by the shared, admin-configurable "Email Model" chrome (header/footer), instead of being the whole email (reverses the #649 "no locked chrome" send path). The shipped default body was already content-only, so it renders unchanged inside the chrome; per-form custom bodies now gain the shared header/footer too. Completes the plugin-wide email consolidation — every plugin email now shares one configurable chrome.
  • Recruitment convocation email (#662) — the editable body is now the "email body" wrapped by the shared, configurable "Email Model" chrome (header/footer) like every other plugin email, instead of being the whole email. Its editor in Recruitment → Settings moved from a plain <textarea> to the TinyMCE visual editor and gained a "Restore Default Text" button. The text/plain alternative is still derived from the body. Existing custom bodies keep working — they simply render inside the shared chrome now.
  • Internal (#662) — the default email "email body" (inner body) templates now load through one shared Core\EmailTemplates loader. The audience booking/cancellation default bodies moved out of AudienceNotificationHandler into templates/emails/audience-{booking,cancellation}.php, and the reregistration handler's bespoke load_template() was folded into the shared loader. Behavior-preserving.
  • Internal (#662) — retired Scheduling\SchedulingMailer::wrap_html (the class-based <style> chrome). Audience and reregistration emails now render through the single, admin-configurable chrome ("Email Model" → ffc_email_document) like every other plugin email, and their info-box markup was inlined (Gmail/Outlook-safe). Behavior-preserving apart from the unified look.
  • Internal (#653) — retired the catch-all Scheduling\EmailTemplateService, splitting it into two focused classes: Scheduling\IcsGenerator (RFC 5545 .ics invite/cancellation building) and Scheduling\SchedulingMailer (the shared HTML chrome + ffcertificate_scheduling_email filter + transport for audience/reregistration emails). The dead render_template (single-brace engine, superseded by Core\TokenResolver) and the format_date/format_time passthroughs were dropped — reregistration now calls Core\DateFormatter::format_date directly. Behavior-preserving; the ffcertificate_scheduling_email filter contract is unchanged. Completes the email-architecture consolidation.
  • Internal (#653) — the self-scheduling appointment emails (booking confirmation, admin notification, approval, cancellation, reminder) no longer build their HTML by string concatenation inside AppointmentEmailHandler; each body moved to a templates/emails/appointment-*.php partial, wrapped by a shared chrome shell templates/emails/layout.php (the single source of the email header band + site-name footer, replacing the ffc_email_header() / ffc_email_footer() trait helpers). The handler is now a thin data-prep orchestrator that renders partials via two new EmailHelperTrait helpers (ffc_render_email_partial() / ffc_email_document()). Behavior-preserving.
  • Internal (#653) — reregistration email templates (templates/emails/reregistration-*.php) migrated from single-brace {token} to the plugin-wide {{token}} engine (via Core\TokenResolver), replacing the last use of EmailTemplateService::render_template. These templates are shipped files (not admin-editable), so no data migration is needed.
  • BREAKING (#653) — audience booking/cancellation email templates now use the plugin-wide double-brace token syntax ({{user_name}}) instead of single-brace ({user_name}), so audience emails share the one placeholder engine. Templates customized and stored per schedule are converted automatically by a one-shot, version-flagged migration (ffc_audience_email_tokens_migrated_v1) — only the known audience tokens are rewritten, so literal braces in your markup (CSS, etc.) are left untouched. External integrations that assemble these templates with the old {token} syntax must update to {{token}}.
  • Internal (#653) — all outbound email now funnels through a single transport chokepoint Core\EmailService::send(), replacing the three prior paths (EmailHelperTrait::ffc_send_mail, EmailTemplateService::send, and raw wp_mail calls in the capability-manager, recruitment dispatcher and self-scheduling CPT). Behavior-preserving — each caller keeps its own headers/content-type (text/html, default, or recruitment's multipart).
  • Internal (#653) — introduced the shared Core\TokenResolver (single-pass {{token}} substitution) and Generators\TemplateRenderer (composes the token + validation-URL-DSL pipeline for emails), and routed the certificate email and recruitment dispatcher through them, replacing their bespoke str_replace / strtr substitution. First step of the email-architecture consolidation; no behavior change (single-pass substitution is marginally safer than the prior sequential str_replace).
  • The submitter email is now fully driven by its editable/translatable template — subject and body substitute {{name}}, {{form_title}}, {{auth_code}} and {{date}}, and the {{validation_url …}} link DSL now runs in emails as well (it had been removed), so the magic download link and /valid verification link can be placed anywhere in the body (e.g. {{validation_url link:m>"Download (PDF)"}}). Substitution runs before sanitising and tolerates TinyMCE-encoded braces. The previously hardcoded heading/auth-code card/button chrome was removed from the send path — the shipped default template (English source, Loco-translatable) now carries all of it. (#649)
  • The {{validation_url …}} DSL parser now keeps double-quoted custom text with spaces intact (e.g. link:m>"Download document (PDF)"); the previous space-split dropped multi-word custom text. Extracted into the shared ValidationUrlPlaceholders helper used by both the PDF layout and the email. (#649)
  • Internal — audience list/search/count and environment holiday/count query caches now invalidate on write via the shared CacheVersion counter (a monotonic per-domain version folded into the cache key) instead of relying on TTL expiry. These caches are keyed by md5( args ) and can't be enumerated to delete individually, so a stale count or search result could previously survive up to an hour after an audience/environment/holiday mutation; every create/update/delete now bumps the audience version so the next read recomputes. Reuses the helper extracted from the recruitment public-listing cache. (#644)

Fixed

  • Documentation tab (#674, #697) — a content audit of the reorganized pages corrected real staleness against the code: the QR placeholder attribute is error= (not error_level=) with a 50–1000px size range, stale certificate tokens were removed, the recruitment notice status is definitive (grouped by adjutancy) and closed notices stay public, reregistration is delivered on the dashboard over AJAX (no shortcode) with the real read-only endpoints, the Forms API uses per_page/page, and Short URLs is a top-level admin menu.
  • Documentation tab (#697) — removed the whole-card collapse behavior from the Quick Navigation: no more disclosure triangle or fold-to-strip on scroll or click. The card stays a fully-expanded sticky panel and only the tree branches open and close, one at a time.
  • Self-scheduling "Confirmation Email" subject/body were editable in the calendar editor but never used — the booking confirmation always sent a fixed built-in template, silently ignoring what admins typed. Now a non-empty confirmation body/subject is honoured: it becomes the email's editable "email body" (tokens {{user_name}}, {{user_email}}, {{calendar_title}}, {{appointment_date}}, {{appointment_time}}) wrapped by the shared Email Model chrome. Leaving it empty keeps the built-in default (with receipt/cancel buttons), so existing calendars are unaffected. The body editor moved to TinyMCE and gained a "Restore Default Text" button. (#662)
  • The global "disable all emails" kill-switch is now bypass-proof — enforced inside the single transport chokepoint Core\EmailService::send() rather than relying on each caller to check it. Recruitment convocation emails, audience/self-scheduling calendar notifications, the capability-manager and the certificate send-site did not all gate on the toggle, so turning emails off did not fully silence outbound mail; every path now honours it. (#662)
  • Form editor → Email tab: the "Notify Admin on Submission" toggle failed to auto-save ("failed to save") — the toggle was wired for incremental autosave but its key was missing from the FormMetaAjaxEndpoint allowlist, so flipping it returned a 403 and the choice only persisted through a full form save. Added send_admin_email to the allowlist. (#660)
  • Self-scheduling calendar editor: the five email-notification toggles rendered on a single line — a more-specific base .ffc-toggle rule (display: inline-flex) overrode the intended per-line stacking. The .ffc-email-toggles container is now a flex column, so each toggle sits on its own row without a specificity/!important fight. (#660)
  • Self-scheduling appointment reminder email was never sent — the reminder handler and its whole read/mark pipeline existed, but nothing scheduled a scan or fired the reminder hook, so enabling "Send reminder before appointment" did nothing. Added the missing hourly cron driver (ffcertificate_self_scheduling_reminder_scan): it finds confirmed, not-yet-reminded appointments due per their calendar's reminder_hours_before, fires the reminder email, and marks them sent (no duplicates); it no-ops when a calendar has reminders off or when emails are globally disabled. (#650)
  • Certificate confirmation email to the submitter was never sent — the async handler was hooked to ffcertificate_process_submission_hook but nothing scheduled it (orphaned since a refactor), so no user email (nor admin notification) went out. Submissions now schedule that dispatch again. (#649)

Security

  • Activity-log export least-privilege (#711) — bulk CSV export of the audit trail now requires a dedicated ffc_export_activity_log capability instead of riding the read-only ffc_view_activity_log, so a view-only operator can read the log but no longer extract it. A one-shot migration grants the new cap to every current ffc_view_activity_log holder, preserving behavior on upgrade. New capability: external integrations that enumerate FFC capabilities should account for the new slug.
  • Settings least-privilege: the two most sensitive Settings surfaces are carved out of the blanket ffc_manage_settings into dedicated capabilities so each can be delegated or withheld independently (#711). ffc_manage_settings_smtp now gates saving the SMTP transport and the Email Model; ffc_manage_settings_dangerzone gates every destructive maintenance action — delete-all submissions, obsolete-shortcode and short-URL cleanup, the public-access disabler, the submission-link audit, and migration execution (the ?ffc_migration runner moves off manage_options-only to this delegable cap). A one-shot migration grants both sub-caps to every existing ffc_manage_settings holder, so no current admin loses access on upgrade — administrators restrict a role by removing a sub-cap. New capabilities: external integrations that enumerate FFC capabilities should account for the two new slugs.