chore(deps)(deps-dev): bump yoast/phpunit-polyfills from 2.0.5 to 4.0.0 - #11
chore(deps)(deps-dev): bump yoast/phpunit-polyfills from 2.0.5 to 4.0.0#11dependabot[bot] wants to merge 1 commit into
Conversation
LabelsThe following labels could not be found: Please fix the above issues or remove invalid values from |
|
Review manual (Sprint 0 follow-up). Não mergeado agora. Esta atualização cruza um limite estratégico que precisa de decisão do maintainer. AnáliseO bump de Porém, PHP 7.4 segue vermelho, falhando durante a execução dos testes (não no install). Isso representa uma quebra efetiva do suporte à versão mínima declarada do plugin ( Caminhos
Assim que houver decisão, retomo. Generated by Claude Code |
Bumps [yoast/phpunit-polyfills](https://github.com/Yoast/PHPUnit-Polyfills) from 2.0.5 to 4.0.0. - [Release notes](https://github.com/Yoast/PHPUnit-Polyfills/releases) - [Changelog](https://github.com/Yoast/PHPUnit-Polyfills/blob/4.x/CHANGELOG.md) - [Commits](Yoast/PHPUnit-Polyfills@2.0.5...4.0.0) --- updated-dependencies: - dependency-name: yoast/phpunit-polyfills dependency-version: 4.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
708995f to
450f602
Compare
|
Closing as obsoleted by #15, which pinned Generated by Claude Code |
|
OK, I won't notify you again about this release, but will get in touch when a new version is available. If you'd rather skip all updates until the next major or minor version, let me know by commenting If you change your mind, just re-open this PR and I'll resolve any conflicts on it. |
…ation alerts (#564) CodeQL (security-extended, JavaScript) open alerts: - #9 / #10 js/xss-through-dom — ffc-reregistration-admin.js audience transfer list built HTML by string-concatenating audience name/color/id read from a DOM data-attribute, then injected with .append() (both the selected and available branches). A malicious audience name/color stored by a delegated audience manager could execute in the reregistration admin screen. Rebuilt with jQuery $('<el>',{...}) + .text()/.attr() so all values are escaped; the color is applied only when it matches a hex pattern. DOM shape, data-id and hidden-input values are unchanged (tests green). - #13 js/incomplete-sanitization — ffc-geofence-admin.js derived the field key with .replace(']','') (first-occurrence only). Input is a hardcoded allowlist so it was not exploitable, but replaced with a robust slice() to clear the alert for good. Rebuilt the matching .min.js. ESLint clean; full Vitest green (1590); JS line coverage 99.41% (floor 96). Note: alerts #11/#12 (js/xss-through-dom in ffc-pdf-generator.js) are false positives — $img.attr('src', …) restoring an <img> src for html2canvas is not an HTML/script-execution sink — and should be dismissed as "False positive" in the code-scanning UI rather than patched on the certificate-render path. Claude-Session: https://claude.ai/code/session_014QgDNjPLKv2gQWMhdG7d6d Co-authored-by: Claude <noreply@anthropic.com>
#870) * fix(templates): enforce read-only default templates server-side (#865) Closes a deviation from #865 decision #11: the #869 edit metabox let an operator edit/rename/delete a shipped default template, but defaults must be read-only — customizing one means Duplicate → an editable user template. - save_edit_metabox: never rewrite a default's HTML (skips update_html; the textarea is also rendered `readonly` with an explanatory notice). Visibility stays togglable for defaults (a default can be hidden, not edited). - protect_default_caps (map_meta_cap, priority 20 so it runs after CptCapPolicy's map at 10): returns do_not_allow for delete_post on a default — a real server-side block, not just hiding the Trash/Delete row action. - preserve_default_title (wp_insert_post_data): restores a default's shipped title on save, blocking rename. Tests cover all three guards plus the readonly render. User templates are unaffected (edit/rename/delete as before). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * fix(templates): avoid casting get_post_field to string in preserve_default_title (#865) PHPStan level 8: get_post_field() is typed array<int>|int|string, so the blind (string) cast could stringify an array. Guard with is_string() instead — functionally identical for post_title (always a string), PHPStan-clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * test(templates): stub get_post_meta in the pre-existing metabox-save test (#865) The #870 read-only guard added a CertTemplateReader::is_default() call (→ get_post_meta) at the top of save_edit_metabox, so the pre-existing test_save_edit_metabox_persists_html_and_visibility (from #869) now needs get_post_meta mocked. Return '' (a non-default template) so the HTML update still runs and the assertion holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 --------- Co-authored-by: Claude <noreply@anthropic.com>
…port / Restore defaults (#865) (#877) * feat(admin): certificate-template management actions — Duplicate / Export / Restore defaults (#865) Completes the ffc_cert_template management screen's row/toolbar actions (#865 decisions #11/#13): - Duplicate (row action, every template): copies the HTML into a new editable user template via CertTemplateWriter::create() and opens it — the sanctioned way to customize a read-only shipped default. - Export (row action, every template): streams the raw template HTML as a `.html` download; the terminal header/echo/exit is isolated in a protected emit_download() so it stays unit-testable. - Restore defaults (toolbar button via restrict_manage_posts): re-runs the seeder non-destructively — CertTemplateSeeder::restore() re-adds any missing shipped default and refreshes the shipped body of existing ones, never touching user templates or a default's chosen visibility. - New templates created via "Add New" (an auto-draft) are visible by default (#865 decision #10). All gated by ffc_manage_forms; defaults stay delete/rename/edit-protected server-side. Seeder refactor extracts insert_default() + existing_default_map() shared by seed()/restore(). Tests cover the row actions, each handler (cap/nonce, duplicate title, export filename/body, restore denial) and the non-destructive restore; no new module-boundary edges (all within Admin). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * style(admin): satisfy WPCS on the template management actions Simplify CertTemplateSeeder::insert_default() to (slug, title, html) all-string params (fixes @PARAM type-alignment errors from the shaped-array type) and add the blank line before each $actions[...] assignment so the short $dup_url / $exp_url temporaries aren't force-aligned to the long array-key writes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 --------- Co-authored-by: Claude <noreply@anthropic.com>
…r REST API, encryption key rotation (#897) * feat: self-hosted plugin updater via GitHub Releases (#820) (#821) * feat: self-hosted plugin updater via GitHub Releases (#820) Teach WordPress's native plugin-update check to see this plugin's GitHub Releases: the update surfaces in Dashboard -> Updates, the details modal renders the release changelog, and WP's per-plugin auto-update toggle becomes available (opt-in; auto_update_plugin is never forced). - Integrations\GithubUpdater hooks pre_set_site_transient_update_plugins (response when newer, no_update when current so the toggle shows), plugins_api (details), and upgrader_pre_download (SHA-256 verify). - Points package at the built ffcertificate-X.Y.Z.zip release asset (not the source zipball -> correct folder, no dev cruft), 12h site-transient cache + ETag If-None-Match against the 60/h rate limit, and aborts the install on a checksum mismatch. - Update URI header added so wordpress.org can't hijack the slug. - Wired always-on in Loader (not is_admin: the scan runs in cron). - 15 unit tests (mock wp_remote_get/transient); class at 95% line coverage. No new module-boundary edge (Root->Integrations exists). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BWjtbKXTR3yo22dMVM5p8 * fix(updater): drop redundant null-coalesce flagged by PHPStan The empty($raw['package']) guard already proves the offset exists and is truthy, so $raw['package'] ?? '' tripped PHPStan's nullCoalesce.offset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BWjtbKXTR3yo22dMVM5p8 --------- Co-authored-by: Claude <noreply@anthropic.com> * chore(activator): flag-guard the FK migration + document the integrity policy (#822) (#823) The user_id -> wp_users foreign-key migration ran on every activation, writing an all-skipped migration_foreign_keys audit-log line each time — the only migration in the activator without a completion-flag guard. - Guard maybe_add_foreign_keys() on ffc_foreign_keys_db_version like the sibling migrations; pin the version only once every FK exists so a MyISAM host / not-yet-created table keeps retrying (renamed from the private add_foreign_keys(); now public + testable). - Document why the migration is still necessary (dbDelta can't emit FKs -> it's the sole path, absent on fresh installs) and that the covered set is a deliberate subset mirroring UserCleanup + the activity_log audit table, not every user_id column. - Correct the recruitment activator's misleading 'no FK = the plugin convention' note; add the policy to CLAUDE.md section 4. - 3 guard tests. Behaviour-preserving; the known reregistration/ custom-field user_id gap is tracked in #822 (out of scope here). Claude-Session: https://claude.ai/code/session_019BWjtbKXTR3yo22dMVM5p8 Co-authored-by: Claude <noreply@anthropic.com> * chore(deps): update vendored thumbmarkjs 1.9.1 -> 1.10.1 (#809) (#824) Drop-in dependency refresh. The UMD API surface is identical (ThumbmarkJS.getFingerprintData / setOption / stableStringify), so ffc-device-signals.js needs no logic change — only the version constant and a path comment. Verified the privacy-critical setOption('logging', false) still gates the sampling beacon to api.thumbmarkjs.com in 1.10.1 (logging && !sessionStorage && random < 1e-4). Vendored file integrity cross-checked: jsdelivr == unpkg for 1.10.1, and the CDN 1.9.1 matched the previously-vendored file byte-for-byte. 20 device-signals JS tests green; minified bundle unchanged (comment-only source edit). Claude-Session: https://claude.ai/code/session_019BWjtbKXTR3yo22dMVM5p8 Co-authored-by: Claude <noreply@anthropic.com> * chore: remove the CptEditorCompat deprecation shim (#739) (#825) Scheduled removal (REMOVE_IN=6.18.0). The shim re-granted ffc_manage_forms / ffc_manage_calendars to WordPress Editors (edit_others_posts holders) for a two-release window after the 6.16.0 CPT-capability decoupling. That window has closed, so remove the class, its Loader init() call, and its test. WP Editors now need the explicit FFC role/capability to manage forms/calendars — completing the #739 security decoupling. CptCapPolicy (the permanent per-post write gate) stays; its docblock reference to the removed shim is reworded. Claude-Session: https://claude.ai/code/session_019BWjtbKXTR3yo22dMVM5p8 Co-authored-by: Claude <noreply@anthropic.com> * fix(admin): diagnosable user-role toggle + honest chips for WP admins (#826) On the user-edit / profile screen the per-user FFC role toggle collapsed every AJAX failure into one generic "reload and try again" alert, so a #739 policy rejection (a WordPress administrator may only be granted the FFC Administrator role — every other role returns 409 cannot_edit_admin) was indistinguishable from an expired nonce. - JS: map the server's reason (cannot_edit_admin / role_not_assignable / forbidden / user_not_found), detect the bare `-1` nonce die as a session-expired message, and use a network-specific message on reject. - PHP: localize the reason-specific strings; render the non-aggregator role chips disabled (omitting ones the admin doesn't hold) for a manage_options target, mirroring the server policy so the UI never offers a click that always 409s. - Tests: JS coverage for every reason branch; PHP coverage for the aggregator-only + held-disabled admin rendering. Claude-Session: https://claude.ai/code/session_019BWjtbKXTR3yo22dMVM5p8 Co-authored-by: Claude <noreply@anthropic.com> * chore: remove scheduled legacy shims — pre-6.2.0 cap rename + pre-4.6.15 cron cleanup (#809) (#827) Fase 2 of #809 retires the two shims the §5 audit log scheduled for 6.18.0: - ensure_legacy_caps_renamed() + CapabilityMigrator::migrate_legacy_certificate_caps() / legacy_cap_renames(): the pre-6.2.0 view_own_certificates → ffc_view_own_certificates rename, dormant on every install that passed through 6.2.0–6.17.0. A site jumping straight from pre-6.2.0 to 6.18.0 skips it (documented in the CHANGELOG banner). - The pre-4.6.15 orphan-cron cleanup (3× wp_clear_scheduled_hook) in the activator and both deactivator paths — those hooks have not been registered since 4.6.15, so any leftover entry is inert. Tests updated: drop the legacy row from the Loader migration provider, the legacy_cap_renames assertion, the activate() legacy-cron test, and the legacy hooks from the deactivate()/uninstall cron-clear counts (6→3, 7→4). CLAUDE.md §5 inventory emptied (both rows removed). Claude-Session: https://claude.ai/code/session_019BWjtbKXTR3yo22dMVM5p8 Co-authored-by: Claude <noreply@anthropic.com> * test(audience): cover NotificationHandler flows + AdminBookings render (#809 Fase 2) (#828) Tests only, no product change. - AudienceNotificationHandlerFlowTest (new, process-isolated): the two orchestration entry points (booking created / cancelled) and their helpers — admin-notification opt-in, recipient CSV parse + site-admin fallback, ICS attachment temp-file lifecycle, and the per-user send loop with skip-on-missing-email. Lines ~16% → ~96%. - AudienceAdminBookingsRenderTest (new, process-isolated): the populated render_page() path — active vs cancelled rows, batched creator-name map with Unknown fallback, known/unknown booking-type labels, export button gated on ffc_export_audiences, and the environment filter branch. Lines ~51% → ~97%. Both new classes run @runTestsInSeparateProcesses because they alias-mock the reader/repository/mailer collaborators; the existing sibling tests (real collaborators) are left untouched. Claude-Session: https://claude.ai/code/session_019BWjtbKXTR3yo22dMVM5p8 Co-authored-by: Claude <noreply@anthropic.com> * fix(admin): preserve FFC roles across WordPress' native profile save (follow-up #826) (#829) The per-user FFC role chip applies the role via AJAX as an *additional* role, but WordPress' native "Update user" save posts the single-role `<select name="role">`; core then runs WP_User::set_role(), collapsing a multi-role user to that one role and silently dropping the FFC role. This hit plain and administrator users alike. Snapshot the user's FFC preset roles on personal_options_update / edit_user_profile_update (which fire before edit_user()/set_role), then re-apply the dropped ones on profile_update (fires after set_role). Only roles the user already held are restored — never a fresh grant — and the snapshot is consumed once per request. Per-user capability grants and audience memberships were already unaffected (set_role only swaps role keys, not the individual cap grants). Claude-Session: https://claude.ai/code/session_019BWjtbKXTR3yo22dMVM5p8 Co-authored-by: Claude <noreply@anthropic.com> * chore(deps)(deps-dev): bump globals from 17.7.0 to 17.8.0 (#831) Bumps [globals](https://github.com/sindresorhus/globals) from 17.7.0 to 17.8.0. - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v17.7.0...v17.8.0) --- updated-dependencies: - dependency-name: globals dependency-version: 17.8.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps)(deps-dev): bump phpstan/phpstan from 2.2.5 to 2.2.6 (#830) * chore(deps)(deps-dev): bump phpstan/phpstan from 2.2.5 to 2.2.6 Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) from 2.2.5 to 2.2.6. - [Commits](https://github.com/phpstan/phpstan-phar-composer-source/commits) --- updated-dependencies: - dependency-name: phpstan/phpstan dependency-version: 2.2.6 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> * fix(recruitment): drop redundant null-check flagged by PHPStan 2.2.6 PHPStan 2.2.6 (bumped in this PR) tightens notIdentical.alwaysTrue detection: `isset( $x ) && null !== $x` is redundant because isset() already excludes null, so the second operand is always true. Simplify both preview_reason_id guards to `isset( $x )`; behavior is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BWjtbKXTR3yo22dMVM5p8 --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Alex Meusburger <rpgmem@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> * chore(deps)(deps-dev): bump eslint from 10.7.0 to 10.8.0 (#832) Bumps [eslint](https://github.com/eslint/eslint) from 10.7.0 to 10.8.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.7.0...v10.8.0) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.8.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * docs(#822): record user-deletion retain-orphan policy + fix appointments migration table name (#833) Resolves the #822 decision for `ffc_reregistration_submissions`: its NOT NULL `user_id` is an **accepted orphan** — a reregistration is a retained record and the identifying PII lives in the `data` JSON body, so nulling the FK alone would be a half-measure. Documented at the source (reregistration activator) and, plugin-wide, in CLAUDE.md §4 with the governing "retain records, drop relationships" principle plus the full inventory of still-uncovered user references (attribution `*_by` columns, post-promotion recruitment candidate, row-body PII cleanup) — correcting the prior note that implied a separate custom-field value table (values live in the `data` column, not a separate table). Also corrects `maybe_migrate_sibling_instants_to_unix()` to target the real `ffc_self_scheduling_appointments` table instead of a non-existent `ffc_appointments` (a silent no-op). Behaviour-preserving on every current install — those columns already ship as BIGINT on 6.6.0+ and the migration helper fast-returns on already-int columns. No product change; docs + a comment + one string literal. Claude-Session: https://claude.ai/code/session_019BWjtbKXTR3yo22dMVM5p8 Co-authored-by: Claude <noreply@anthropic.com> * test(#809): scaffold tests/Integration/ with a plugin-bootstrap wiring smoke (#835) Introduce the previously-empty integration test layer (Fase 3 structural gap). PluginBootstrapSmokeTest boots the real plugin object graph via Loader::init_plugin() over a stubbed WordPress boundary and asserts it composes without a fatal and lands its bootstrap hooks / crons / roles, in both the frontend and admin contexts -- the wiring the unit LoaderTest deliberately excludes ("would require mocking the entire plugin graph"). - IntegrationTestCase: reusable base that installs a recording WP boundary (add_action/add_filter/add_shortcode/cron/REST captured; permissive no-op defaults elsewhere) and a helper to seed one-shot migration flags so the boot takes its steady-state path. - Register an `integration` PHPUnit suite (runs in the plain phpunit gate). The coverage shards still glob tests/Unit only, so the coverage floor is untouched. - Add WP table-name properties ($posts, $users, ...) to the wpdb stub in tests/bootstrap.php so repositories reading $wpdb->posts (FormRepository) resolve during a full-graph boot. Claude-Session: https://claude.ai/code/session_019BWjtbKXTR3yo22dMVM5p8 Co-authored-by: Claude <noreply@anthropic.com> * chore(deps-dev): bump wp-coding-standards/wpcs 3.4.0 → 3.4.1 (CVE-2026-45293) (#843) Clears the high-severity advisory CVE-2026-45293 (arbitrary code execution in WordPressCS >=0.14.1,<3.4.1) that was failing the `Composer audit` gate on develop and every open PR. wpcs is a dev/CI-only dependency (the WPCS gate), so this rides the develop batch per the dev/CI-only-dep policy rather than a hotfix. Lock-only change within the existing `^3.1` constraint; also pulls the matching phpcsstandards/phpcsextra 1.5.1 + phpcsutils 1.2.3. Verified locally: `composer audit` reports no advisories after the bump. Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-authored-by: Claude <noreply@anthropic.com> * security(csv): minimize public export + noindex /valid (#836) (#842) Hardens the public certificate CSV export ([ffc_csv_download]) and the /valid verification page. - V2: drop the magic-link `Token` column from the public CSV row. The token is a live access credential (the magic link renders the full-PII PDF) and was being dumped in bulk for every submission. - P1: drop the User IP, Consent Given/Date/IP/Text and Status columns from the public export — data minimization. The admin export (SubmissionsExportSource) keeps its full layout; the two are no longer byte-identical by design. - P2: emit `noindex` on the [ffc_verification] page (configured page id or shortcode) via the wp_robots filter — belt-and-suspenders on top of the fragment-based magic link. Public CSV column layout changed (8 fixed columns: ID, Form, User ID, Submission Date, E-mail, CPF, RF, Auth Code). Tests updated for the new layout; added coverage for verification_page_robots(). Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-authored-by: Claude <noreply@anthropic.com> * security(js): encode quotes in attribute-context escaping (#837) (#844) Two JS attribute-context escaping gaps (same bug class as the #564 stored-XSS), fixed mechanically: S4 (stored XSS, user → admin) — ffc-admin-submission-edit.js built its user-search result data-*="…" attributes with a createTextNode+innerHTML escapeHtml that encodes < > & but NOT quotes. A logged-in user's display_name (sanitize_text_field keeps quotes) is echoed unescaped by the admin AJAX search, so a value like `a" onmouseover="…"` broke out of the attribute and ran a handler in the admin's session. escapeHtml now also encodes " and '. S12 (self-XSS) — ffc-user-dashboard-profile.js used esc() (no quote encoding) inside the edit-form value="…" inputs and the audience-group chip style="…". Switched those five sites to the attribute-safe escAttr helper already defined in the dashboard core. Rebuilt the two .min.js bundles. Added Vitest regression coverage: a quote-laden display_name in the admin search and in the profile edit form / group chip no longer injects a rogue onmouseover attribute. CHANGELOG under [Unreleased] → Security. Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-authored-by: Claude <noreply@anthropic.com> * security(rest): mask PII on /submissions + /verify, reuse the /valid masks (#838) (#845) Two REST endpoints returned certificate PII unmasked, bypassing the "mask unless a PII cap is held" convention the wp-admin + /valid surfaces already enforce. Fixed by reusing the *existing* masks — no new masking logic: S3 (public POST /verify) — the endpoint masked the top-level email/cpf/rf columns but returned the decrypted `data` blob raw, leaking every form-collected PII field (cpf/rf/rg/email inside `data`) to any caller with a valid auth code. It now runs `data` through the same per-key mask the /valid page uses. S2 (GET /submissions[/{id}]) — gated only by the read-only `ffc_view_certificates` (view) tier, it returned raw email + full `data` (single also returned unmasked cpf/rf via format_document). It now masks email/cpf/rf/data by default and returns plaintext only for the unmasked (`ffc_certificates_admin` role / super-admin) and reveal (`ffc_view_certificates_pii` cap) tiers via the shared PiiAccessPolicy, auditing each reveal exactly as the admin reveal endpoint does — so a PII-blind operator can no longer bulk-extract certificate PII over REST without a trail. Reuse detail: the per-key masking that lived inline in VerificationResponseRenderer::format_field_value() is extracted into the shared DocumentFormatter::mask_field_value() (cpf/cpf_rf/rg -> mask_cpf, rf -> mask_rf, email -> mask_email; non-PII fields untouched). Both the /valid renderer and the REST controller now call it, so the two paths can't drift apart again. Top-level fields reuse the existing mask_cpf/mask_rf/mask_email + PiiAccessPolicy. No one-shot cap migration: the roles that legitimately need plaintext (manager, admin) already hold `ffc_view_certificates_pii`; viewer/operator are PII-blind by design, so auto-seeding would defeat the fix. Breaking for any external REST consumer authenticating as a view/operator role — grant them the PII cap to keep plaintext (noted in CHANGELOG). Tests: DocumentFormatterTest covers mask_field_value (PII keys masked, non-PII/empty/non-scalar passthrough); SubmissionVerifyRestControllerTest adds masked-tier + reveal-tier /submissions cases and the /verify data-blob mask. Could not run composer suites locally (proxy blocks composer's GitHub auth); php -l clean, CI runs the authoritative gates. Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-authored-by: Claude <noreply@anthropic.com> * security(hygiene): rate-limit PII hash, CSV import limits, no-JS verify throttle, nginx doc (#839) (#847) * security(hygiene): rate-limit PII hash, CSV import limits, no-JS verify throttle, nginx doc (#839) Four independent low-risk hygiene items from #839 (S7a — the key/salt health warning — is split into sub-issue #846 with its own PR): S5 — ffc_rate_limits stored the rate-limit identifier (email / normalized CPF) in plaintext, unlike the sibling ffc_rate_limit_logs which hashes. RateLimitRepository now routes every identifier through a private storage_identifier(): non-IP types are sha256-hashed at rest (a counter only needs a stable equality key), IP stays plaintext for the stats "top IPs" view. Applied on both the read (get_count / is_blocked) and write (increment / block) sides so lookups stay consistent; existing raw rows lapse after their window (one-time reset at most). S9 — the recruitment classifications CSV import read the whole upload with file_get_contents() with no size or type guard (memory DoS). A shared validate_csv_upload() now enforces an extension allowlist + a 10 MB cap before the read, across all three import entry points (import_csv / import_job_start / definitive-import promote). S11 — the no-JS [ffc_verification] shortcode fallback (VerificationHandler::verify_certificate) skipped the verification rate limit that the AJAX handlers apply, leaving an un-throttled auth-code probing path. It now calls RateLimiter::check_verification before any lookup, matching the AJAX paths. S8 — documented the nginx `deny` rule for the CSV-export temp dir (wp-content/uploads/ffc-tmp/, .htaccess is Apache-only) in a new docs/DEPLOYMENT.md + a readme.txt FAQ, and pointed the in-code comment at it. Tests: RateLimitRepositoryTest (non-IP identifier hashed on write + read, IP plaintext); RecruitmentClassificationsRestControllerTest (bad extension → 400, oversize → 413; realistic $_FILES fixture); VerificationHandlerAjaxTest (no-JS fallback is rate-limited). Could not run composer suites locally (proxy blocks composer's GitHub auth); php -l clean, CI runs the authoritative gates. Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * style(#839): align $we assignment in increment_counter (WPCS) Inserting the S5 storage_identifier() call re-aligned the $t/$ws assignments to the longer $identifier LHS but left the trailing $we line at its old single-space alignment, tripping the WPCS "Equals sign not aligned" sniff. Whitespace-only. Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#839): stop leaking an undefined $_POST across shard neighbours Coverage shard 3 failed on TabRateLimitTest with "Undefined global variable $_POST". Root cause: SelfSchedulingCleanupHandlerTest::tearDown did `unset( $_POST )`, which removes the superglobal entirely and leaves it *undefined* for the next test in the shard. TabRateLimitTest's render()-under-test reads `if ( $_POST … )` and errored. The failure only surfaced now because this PR adds test methods, and the test-count-weighted shard packer reshuffled which test lands after the cleanup test. Fix at the root: reset `$_POST = array()` instead of unsetting it, so no neighbour ever sees it undefined. Also self-initialize `$_POST` in TabRateLimitTest::setUp as belt-and-suspenders. Test-only. Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> * security(encryption): key/salt health warning + status panel (#846, #839 S7a) (#848) * security(encryption): key/salt health warning + status panel (#846, #839 S7a) Adds a non-blocking surface for unsafe encryption secret material — the case where the WordPress secret keys (or the FFC decoupling constants) that derive the encryption key + search-hash salt are missing, weak, or still the wp-config-sample placeholder, which silently makes the key predictable while is_configured() still reports "configured". Core: - Encryption::key_health() → ok|weak|placeholder|empty, evaluating the *active* secrets (honors FFC_ENCRYPTION_KEY / FFC_HASH_SALT decoupling, else the WP salts each half falls back to). - Encryption::key_health_report() → status + decoupled flags + human key/salt sources + a non-reversible key fingerprint (domain-separated HMAC slice) so an operator can confirm a rotation without seeing the key. - get_info() now carries key_health. is_configured() is deliberately UNCHANGED — flipping it to false on a placeholder would disable encryption and store PII in plaintext, which is worse. UI (3 layers, per the issue): - Global dismissible admin notice (EncryptionKeyHealthNotice) shown only when health != ok; dismissal is keyed to a status+fingerprint signature so a worsening state or a key rotation re-surfaces it. Points at the status panel. - Advanced-tab "Encryption Key Health" card (id=ffc-encryption-health, placed directly above the Danger Zone): status, sources, fingerprint, and a soft "decouple with FFC_ENCRYPTION_KEY" advisory when healthy but coupled. Guidance distinguishes a fresh install (generate salts) from a site with data (rotation caution — do not edit WP salts blindly; S7b). - Generic reusable dismiss JS (ffc-dismissible-notice.js on .ffc-js-dismiss-notice) driving the persistent dismissal. Absorbs the old S6 (search-hash reversal) — no hash-algorithm migration (the only mass-reversal scenario needs a weak salt, which this detects). Tests: EncryptionTest (secret classifier via reflection; report shape; get_info); EncryptionKeyHealthNoticeTest (gating, signature-based re-show, ajax dismiss); dismissible-notice.test.js (JS). Could not run composer suites locally (proxy blocks composer's GitHub auth); php -l clean, CI runs the authoritative gates. Note: the notice class still shares the init/dismiss/cap skeleton with DeviceThresholdUpgradeNotice; extracting a shared AbstractDismissibleNotice base is pending a scope decision. Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#846): PHPStan redundant isset + WPCS Yoda/alignment on the key-health notice CI on the S7a PR flagged three lint issues (couldn't run composer locally — proxy blocks its GitHub auth): - PHPStan: two `isset()` guards on key_health_report() offsets that the declared array shape guarantees always exist → dropped, read directly. - WPCS Yoda: `$dismissed !== self::current_signature()` → put the non-variable operand on the left. - WPCS alignment: `$ffc_kh_label` was grouped with the earlier assignment block across the multi-line labels array on the Advanced view; a blank line breaks the group so its single-space `=` is correct. Verified the alignment fix with a standalone phpcs Generic.Formatting.MultipleStatementAlignment run (clean). Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#846): stub wp_kses() in TabAdvancedTest for the S7a card TabAdvancedTest::test_render_includes_real_view renders the real Advanced view, which now includes the Encryption Key Health card. With the unit bootstrap's short WP salts, key_health() is 'weak', so the card's warning branch runs and calls wp_kses() (explicit allowlist) — a function the test didn't stub, tripping "Call to undefined function wp_kses()". Added the stub alongside the existing wp_kses_post one. Test-only. Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> * refactor(admin): AbstractDismissibleNotice base + wp_admin_notice; English updater string (#849) (#852) * refactor(admin): AbstractDismissibleNotice base + wp_admin_notice; en source string (#849) Follow-up after #839/#846 closed (agreed in-session). Notice de-duplication: - New AbstractDismissibleNotice base owns the identical plumbing the two persistent-dismiss admin notices copy-pasted: hook registration, the wrapper markup, the capability gate, the dismissed-state gate, the AJAX dismiss endpoint, and the shared dismiss-script enqueue. Subclasses now supply only their specifics (option key, action, should_show(), dismiss_signature(), notice_message(), type, extra class). - Migrated both DeviceThresholdUpgradeNotice and EncryptionKeyHealthNotice onto it. Behaviour + markup preserved (each keeps its stable ffc-*-notice class via extra_class, so existing tests' class assertions still hold). - Adopted WordPress's canonical wp_admin_notice() (WP 6.4+) for the wrapper instead of hand-building the <div> — core builds the notice/dismissible classes and escapes classes + attributes. Bumped `Requires at least` 6.2 -> 6.4 in the plugin header + readme (the target already runs 6.5+; the l10n catalog is 6.5-format). - Removed the now-dead ffc-device-threshold-notice.js (+ min): the device notice uses the shared ffc-dismissible-notice.js (.ffc-js-dismiss-notice). i18n: - The GitHub-updater SHA-256 mismatch error was hard-coded in Portuguese; the source string is now English so pt_BR flows through the catalog instead of overriding every locale. (pt_BR entry regenerated via Loco.) Tests: added init()/ajax-forbidden coverage for the shared base through the encryption subclass; stubbed wp_admin_notice() in both notice tests; removed the dead device JS test. JS suite 1637/1637, coverage 99.11%. Could not run composer suites locally (proxy blocks composer's GitHub auth); php -l clean, WPCS alignment verified via standalone phpcs, CI runs the authoritative gates. Follow-ups tracked: #850 (wp_admin_notice sweep of inline notice markup), #851 (suggest a random FFC_ENCRYPTION_KEY on the health card). Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#849): WPCS missing docblocks + PHPStan redundant array_values CI on the notice-abstraction PR: - WPCS: the concrete abstract-method implementations in the two notice subclasses lacked doc comments (option_key/action/notice_type/ extra_class/notice_message) — added one-line docblocks. - PHPStan: array_values() in the base's maybe_render() was a no-op (the additional_classes array is already a list — index 0 is always the 'ffc-js-dismiss-notice' constant), so it's dropped. Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> * refactor(admin): adopt wp_admin_notice() across inline notice markup (#850) (#853) * refactor(admin): adopt wp_admin_notice() across inline notice markup (#850) Sweep the plugin's ~50 hand-built `<div class="notice …">` admin-notice snippets onto WordPress's canonical `wp_admin_notice()` (WP 6.4+, established by #849), so type / dismissibility / additional-class markup is core-escaped and consistent instead of copy-pasted per call site. Covers settings tabs + views (incl. the S7a Encryption Key Health card and the 11-notice migrations view), the admin pages (settings, role editor, submission-edit, activity-log, form-editor save errors, bulk-move notice), recruitment edit pages + admin renderer, self-scheduling admin + appointments list, and the recruitment welcome / url-shortener templates. Dynamic-type and rich-body notices (with <strong>/<ul>/links) use the `paragraph_wrap => false` + `ob_start()`/`ob_get_clean()` capture form so the readable interspersed markup stays intact. Behaviour-preserving. Tests: stub `wp_admin_notice()` (a faithful marker echoing the canonical `notice notice-{type}` markup) in every test that renders a migrated path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * fix(admin): satisfy PHPStan + WPCS on the wp_admin_notice sweep (#850) - PHPStan: cast `(string) ob_get_clean()` at every `wp_admin_notice()` call site (ob_get_clean returns string|false; the param expects string) — activity-log-page, submission-edit, form-editor save errors, and the advanced/general/migrations views + recruitment-welcome template. - WPCS ControlSignature: split `ob_start()` off the alternative-syntax branch line (`if (…) : ob_start(); ?>` → `if (…) : ?>` + its own `<?php ob_start(); ?>`) in the advanced + general views. - WPCS I18n: expand the QR-cache notice to the multi-line sprintf form so the `translators:` comment sits directly above the `__()` call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 --------- Co-authored-by: Claude <noreply@anthropic.com> * feat(settings): FFC_ENCRYPTION_KEY suggestion on the Encryption Key Health card (#851) (#856) On Settings → Advanced, whenever FFC's encryption key still derives from the shared WordPress salts (encryption_decoupled === false — any status while not decoupled: ok / weak / placeholder / empty), the Encryption Key Health card now offers a ready-to-paste `define( 'FFC_ENCRYPTION_KEY', '…' )` snippet in a read-only field, with "Generate another" and Copy buttons. - The 64-char key is generated client-side (crypto.getRandomValues) over a charset safe for a single-quoted PHP literal (excludes ' " \ $) and is never saved or sent — the plugin never writes wp-config.php (the S7a decision). - Mandatory rotation caveat: apply directly only on a new install; a site with data whose salts are weak/placeholder needs a planned rotation (S7b). - Hidden once already decoupled (nothing to suggest). - New vanilla-JS `ffc-encryption-key-suggest.js` (pure generator exposed on window for testing) enqueued only on the Advanced tab + a Vitest suite (length, safe charset, regenerate changes value, copy feedback). Builds on #846 / #839 S7a. Behaviour-preserving; purely additive UX. Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-authored-by: Claude <noreply@anthropic.com> * chore(deps)(deps-dev): bump phpstan/phpstan from 2.2.6 to 2.2.7 (#854) Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) from 2.2.6 to 2.2.7. - [Commits](https://github.com/phpstan/phpstan-phar-composer-source/commits) --- updated-dependencies: - dependency-name: phpstan/phpstan dependency-version: 2.2.7 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Alex Meusburger <rpgmem@gmail.com> * chore(deps)(deps-dev): bump jsdom from 29.1.1 to 30.0.1 (#855) Bumps [jsdom](https://github.com/jsdom/jsdom) from 29.1.1 to 30.0.1. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Commits](https://github.com/jsdom/jsdom/compare/v29.1.1...v30.0.1) --- updated-dependencies: - dependency-name: jsdom dependency-version: 30.0.1 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Alex Meusburger <rpgmem@gmail.com> * security(encryption): key rotation migration (S7b) + WP-derived decrypt fallback (#857) (#859) * security(encryption): key rotation migration (S7b) + WP-derived decrypt fallback (#857, #840) Adds the S7b encryption-key rotation feature (split from #840 Part A): - Encryption::decrypt() gains a WP-derived-key fallback so rows encrypted under the WordPress salts stay readable the moment a strong FFC_ENCRYPTION_KEY is defined (zero downtime), until re-encrypted. Key derivation is refactored behind is_decoupled() + wp_derived_* helpers; a parameterized try_decrypt()/encrypt_with_keys() seam backs both the active key and the fallback. New public key_fingerprint(). - New KeyRotationMigrationStrategy (Architecture A): re-encrypts submissions + appointments PII under the active key and rebuilds the CPF/RF/email search hashes under the active salt. Cursor-based progress, keyed on the non-reversible key fingerprint so changing the key re-arms the migration instead of silently reporting complete; a row it cannot decrypt is reported as an error and left untouched (never orphaned). - Registered in MigrationRegistry + MigrationStatusCalculator; the Settings -> Migrations card auto-renders. Execution is gated by the existing ffc_manage_settings_dangerzone AJAX/no-JS endpoints; can_run blocks until FFC_ENCRYPTION_KEY is decoupled. Tests: Encryption fallback/mechanism/fingerprint (3), KeyRotation strategy (10), registry/calculator updates. CHANGELOG Security entry. No assets changed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * fix(s7b): satisfy WPCS translators comment + PHPStan constant guard - Move the translators comment directly above the __() call inside the process_table sprintf so WPCS.WP.I18n accepts it (the __() was on its own wrapped line). - Read FFC_ENCRYPTION_KEY via constant() at the three usages now guarded by is_decoupled() instead of an inline defined() — PHPStan's constant.notFound only recognises a same-expression defined() guard. Behaviour-identical at runtime. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 --------- Co-authored-by: Claude <noreply@anthropic.com> * feat(api): authenticated operator REST API — issuance + PDF (#858) (#860) * feat(api): authenticated operator REST API — issuance + PDF (#858, #840 Part B) Adds two ffc/v1 routes for a field-operator app authenticating via WordPress Application Passwords, in a new OperatorCertificatesRestController: - A1 POST /operator/certificates — issues a certificate, gated by ffc_manage_certificates (admins via manage_options). Reuses the same SubmissionHandler + field/CPF/email validation as the public form, but deliberately omits the anti-anonymous defenses (captcha/geofence/per-IP+ email+CPF rate-limit) — inappropriate for an authenticated, cap-gated caller — applying a generous per-user_id rate-limit as a runaway guard plus an audit-log entry per issuance. - A2 GET /operator/certificates/{id}/pdf — returns the certificate's pdf_data (the same client-rendered payload the web path uses; the plugin has no server-side PDF binary renderer), gated by the management capability OR ownership of that certificate. Additive: the public POST /forms/{id}/submit and the admin-ajax handlers are untouched. A5 (migrating specific admin-ajax ops to REST) is deferred until a concrete app driver exists. Wired in RestController::register_routes(). 14 unit tests cover route registration, both permission gates, the issuance happy path + validation/ rate-limit/not-found error paths, and PDF cap/ownership/forbidden/not-found. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * fix(api): WPCS const alignment + drop redundant is_array (#858) - Align the RATE_* const equals signs (WPCS MultipleStatementAlignment). - Drop the redundant `! is_array( $params )` after `empty( $params )` — get_json_params() is typed array, so PHPStan flags it as always-true. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 --------- Co-authored-by: Claude <noreply@anthropic.com> * security(user-deletion): attribution + promoted-candidate coverage (#834) (#861) Follow-up to #822. Extends the deleted_user hook (UserCleanup) with an authorship sweep that SET-NULLs, on every deletion: - the nullable actor-attribution columns — submissions.edited_by; appointments.approved_by/cancelled_by; self_scheduling_calendars .created_by/updated_by; blocked_dates.created_by; audience_bookings .cancelled_by; reregistration_submissions.reviewed_by; recruitment_call .cancelled_by; short_urls.created_by (cluster 1); and - recruitment_candidate.user_id, the promoted-candidate link (cluster 2). The sweep runs outside the #322 data-subject footprint short-circuit because a pure actor (admin/operator) or promoted candidate need not have their own footprint. Identifiers go through %i (no interpolation); each UPDATE is guarded by table_exists + column_exists. The six NOT NULL created_by columns stay accepted orphans (a plain SET NULL would be rejected). App-layer only — no FK backstop for attribution columns. Cluster 3 (row-body PII): documented as a decided policy, not a code change — routine account deletion anonymises the link only; scrubbing the encrypted *_encrypted/*_hash body stays the manual PrivacyErasers (LGPD erasure) path, so certificates/appointments remain verifiable until a subject actually requests erasure. CLAUDE.md §4 rewritten to reflect the new coverage; 3 new UserCleanupTest cases; CHANGELOG entry. Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-authored-by: Claude <noreply@anthropic.com> * feat(advanced): FFC_HASH_SALT suggestion + self-documenting snippets + rotation gating (#862) * feat(advanced): FFC_HASH_SALT suggestion + self-documenting snippets + rotation gating Extends the Encryption Key Health panel (Settings → Advanced) and the S7b key-rotation flow, all client-side / no secret ever exposed: - Add a FFC_HASH_SALT suggestion field alongside FFC_ENCRYPTION_KEY. The generator JS is generalised to any number of widgets (data-ffc-const), each shown only until that constant is decoupled. - Each copyable snippet now carries a self-identifying phpdoc comment (constant, purpose, FFC version) mirroring the commented WordPress salt block; the field becomes a textarea to hold the multi-line block. Comment bodies are localised (i18n, FFC_VERSION embedded) via wp_localize_script. - Encryption Key Rotation now requires BOTH FFC_ENCRYPTION_KEY and FFC_HASH_SALT: the migration card (Settings → Migrations) stays hidden and can_run refuses until both are defined — rotating with only the key set would rebuild the search hashes under the still-shared WordPress salt, forcing a second rotation when the salt is later decoupled. - Close the S7b UX gap: once both constants are defined (the generator block hides exactly then), the panel surfaces a pending-record count linking to the rotation card, so the adopt→rotate loop is discoverable. Tests: JS comment/multi-widget coverage (14 suggest tests); strategy can_run now asserts both constant names. Full JS suite 1651 green, 99.03% lines. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * docs(advanced): correct FFC_ENCRYPTION_KEY snippet comment for the one-way constraint The generated snippet's comment said changing the key 'requires a key rotation' — true only for the first WP→FFC adoption. Rotating one FFC key to another is not supported by the current single-WP-derived fallback (tracked as a follow-up), so the comment now states the accurate contract: once records are stored under the key, treat the value as permanent — changing/removing it makes them unreadable until it is restored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * i18n(advanced): keep the wp-config snippet comment English on every locale The generated FFC_ENCRYPTION_KEY / FFC_HASH_SALT snippet comment lands inside wp-config.php (a code file), so it must stay English regardless of the admin's locale — going through __() would emit a translated (e.g. pt_BR) comment into code. Drop the i18n wrappers on the two comment bodies; the copy-feedback UI strings stay localized. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 --------- Co-authored-by: Claude <noreply@anthropic.com> * Language Update * security(encryption): FFC→FFC re-keying via previous-key fallback (#863) (#864) * security(encryption): FFC→FFC re-keying via previous-key fallback (#863) Enables rotating one FFC_ENCRYPTION_KEY to another without downtime — the missing direction after #857 (which only bridged WordPress-derived → FFC). Core (Encryption): - decrypt_internal() gains a previous-key candidate: chain is now active → previous-FFC → WP-derived. When FFC_ENCRYPTION_KEY_PREVIOUS (≥32 chars) is defined, rows still under the prior FFC key stay readable while new writes use the active key; the WP→FFC fallback is unchanged, so both transitions can be in flight at once. - has_previous_key()/previous_encryption_key()/previous_hmac_key() mirror the decoupled active-key derivation; key_fingerprint() refactored onto a shared fingerprint_of(); key_health_report() adds rekey_in_progress + previous_fingerprint. - The salt needs no previous — search hashes are rebuilt from plaintext. Panel (Settings → Advanced): - Previous-key fingerprint row during a re-key. - Rotation notice is re-key-aware: in-progress (run rotation, then remove _PREVIOUS, don't change keys until 100%) vs complete (remove _PREVIOUS). - "Rotate to a new key" helper (decoupled, not re-keying): generates the new key + the step-by-step wp-config edits, reusing the #862 generator. Rotation strategy unchanged — process_table already decrypts via the full chain, so it re-encrypts previous-key rows under the active key automatically. Tests: separate-process re-key decrypt roundtrip + health-report assertions; default no-rekey report. EncryptionTest 35 green; rotation/status/notice suites unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * docs(tab): document Encryption Key Health, decoupling & re-key on the Advanced page (#863) The Documentation → Configuration → Advanced page covered the activity log, editor prefs, debug toggles, download limit and Danger Zone but nothing about the Encryption Key Health card, the FFC_ENCRYPTION_KEY / FFC_HASH_SALT decoupling constants, the Encryption Key Rotation migration, or the new FFC→FFC re-key flow. Adds a focused section: what the panel reports, decoupling as optional hardening, the one-way caveat, and the 4-step re-key procedure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * docs(tab): fill documentation gaps for operator API, privacy/LGPD, modules, activity-log, updates & url-shortener export Cross-checked the in-plugin Documentation tab against shipped features and filled the gaps: - Developer page: document the authenticated operator REST API — POST /ffc/v1/operator/certificates + GET .../{id}/pdf, Application-Password auth, ffc_manage_certificates gate, per-operator rate-limit rationale (#858). - New "Privacy & LGPD" page: PII at rest (AES-256-CBC + HMAC, salted hash, masked display), WordPress export/erase integration (6 exporters + 1 eraser), account-deletion anonymization vs. erasure, suggested policy text. - New "Modules" page: per-module enable/disable, default-on, disable hides screens + stops runtime/crons but never deletes data (#800). - New "Activity Log" page: the viewer tab (filters/stats/CSV export) split from the logging config, gated by ffc_view/export_activity_log (#802). - New "Updates" page: GitHub-Releases auto-update — notify + native opt-in, SHA-256 verify, 12h ETag cache, no token (#820). - Short URLs page: add the missing ffc_export_url_shortener capability and the CSV export (#711/#761). Registry gets four new nodes; no product code touched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * docs(tab): fix WPCS PrecisionAlignment — collapse operator-API curl to one line The multi-line curl example indented its continuation lines with 2 spaces, tripping Universal.WhiteSpace.PrecisionAlignment three times. Collapse to a single line, matching the existing single-line curl example in the same file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 --------- Co-authored-by: Claude <noreply@anthropic.com> * feat(templates): certificate-template pool — DB-backed models (#865, phase 1) (#867) * feat(templates): certificate-template pool data layer — CPT + seeder + reader (#865) First step of the #865 template-pool feature: a database-backed pool of reusable certificate templates, replacing the fragile "drop a .html in the plugin's html/ folder" extension point (wiped on every update). This commit is the data layer only — fully additive and behaviour-preserving (the editor still uses the legacy html/ glob; the rewire lands next): - CertTemplateCpt: registers the `ffc_cert_template` CPT, gated by the FFC `ffc_manage_forms` / `ffc_view_forms` caps via capability_type + map_meta_cap, mirroring `ffc_form`'s #739 decoupling. `show_ui` is false for now (the management submenu is a follow-up phase); per-post writes route through CptCapPolicy (new `ffc_cert_template => ffc_manage_forms` mapping). - CertTemplateSeeder: non-destructive, versioned seed of the three shipped defaults. Canonical HTML source now lives in templates/certificate-defaults/ (copied from html/); seeds only missing default slugs, so it doubles as the future "Restore defaults" without clobbering user edits. - CertTemplateReader: read side for the editor "Load" flow — lists visible templates (defaults first) and resolves a template's HTML by id. - Wired in Loader under the certificates module; seeding on admin_init. Storage contract (meta): _ffc_template_html, _ffc_is_default, _ffc_default_slug, _ffc_template_visible. All in the Admin module — no new module-boundary edge. Tests: CertTemplateCptTest, CertTemplateReaderTest, CertTemplateSeederTest, and a CptCapPolicyTest case for the new mapping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * fix(templates): drop two PHPStan-redundant guards in the pool data layer (#865) CI PHPStan (level 8) flagged two always-true/false checks whose types the WP stubs already guarantee: - CertTemplateReader::list_for_editor — get_posts() (no `fields`) is typed WP_Post[], so the per-item `instanceof WP_Post` is always true; removed it. - CertTemplateSeeder::seed — wp_insert_post( …, true ) returns int<1,max>|WP_Error, so `$id <= 0` is always false; is_int() alone rejects the error case. Behaviour unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 --------- Co-authored-by: Claude <noreply@anthropic.com> * feat(templates): rewire the editor "Load" flow to the DB template pool (#865) (#868) * feat(templates): rewire the editor "Load" flow to the DB template pool (#865) Second step of the #865 template pool (follows the merged data layer): the form-editor "Load" control now reads the ffc_cert_template pool instead of globbing html/, resolving template HTML server-side by post id. - FormEditorLayoutMetabox::render — the layout <select> is populated from CertTemplateReader::list_for_editor(), grouped defaults / user templates, each <option> valued by post id (was: html/ glob keyed by filename). - AdminAssetsManager::discover_layout_templates — returns the reader list ([id,label,is_default,file]) for the "Load" modal; falls back to the legacy html/ glob (id 0 + file basename) when the pool is empty. - FormEditor::ajax_load_template — accepts template_id (int) and returns CertTemplateReader::get_html($id) under the existing ffc_admin_pdf_nonce + current_user_can_admin_or('ffc_manage_forms') gates; keeps the filename path as a deprecated fallback. - ffc-admin-pdf.js — loadTemplateFile now POSTs ffc_load_template with template_id (or the legacy filename when id 0), replacing the unauthenticated direct fetch() of /wp-content/plugins/.../html/; modal options carry data-id. Re-minified. The legacy html/ glob is retained as a documented deprecated fallback, to be removed once the pool is guaranteed seeded on every install. Tests: FormEditorTest (load by id: success + not-in-pool error), AdminAssetsManagerTest (localized catalog keyed by id), FormEditorLayoutMetaboxTest (grouped pool select), admin-pdf.test.js (ajax path + id-based modal + legacy filename fallback). CHANGELOG under [Unreleased] → Changed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * test(templates): stub get_posts in FormEditorMetaboxRendererTest (#865) The layout metabox's "Load" dropdown now lists the DB template pool via CertTemplateReader::list_for_editor() → get_posts(); FormEditorMetaboxRendererTest renders that metabox through render_box_layout / render_tabbed_container but did not mock get_posts, so Brain\Monkey erred with "get_posts is not defined nor mocked" (cascading to the coverage shards). Stub it to an empty pool, matching FormEditorLayoutMetaboxTest / AdminAssetsManagerTest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 --------- Co-authored-by: Claude <noreply@anthropic.com> * feat(templates): certificate-template pool management screen (#865, PR B) (#869) * feat(templates): pool management screen — list columns + visibility toggle (#865) PR B of #865 (management UI), first slice. Surfaces the ffc_cert_template pool in wp-admin and adds the write side the authoring flows need. - CertTemplateCpt: flip show_ui => true and show_in_menu => 'edit.php?post_type=ffc_form', so the pool appears as a "Templates" submenu under the Certificate menu (native list table + edit screen), still gated by the ffc_view_forms / ffc_manage_forms caps. - CertTemplateWriter (new): write side for the pool — create() a user template (html + visible + is_default=0), set_visibility() and update_html(), each fenced to the ffc_cert_template post type. Auth stays the caller's job. - CertTemplateAdminScreen (new): list-table Type (Default/Custom) + Visible columns and a nonce-protected Show/Hide row action that flips META_VISIBLE via the writer (manage cap only; read-only for viewers). Shipped defaults have their Trash/Delete row actions removed (the seeder re-creates them). - Loader: construct CertTemplateAdminScreen under is_admin() alongside the CPT. Tests: CertTemplateWriterTest, CertTemplateAdminScreenTest, and the updated CertTemplateCptTest (show_ui true + submenu). CHANGELOG under [Unreleased] → Added. Follow-ups (same #865 PR B): the HTML-editing metabox on the CPT edit screen and the editor's "Save as model" button + ffc_save_template AJAX. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * fix(templates): satisfy WPCS + PHPStan on the pool admin screen (#865) CI on the foundation slice flagged three issues in CertTemplateAdminScreen that the sandbox (no vendor/) couldn't catch locally: - WPCS: @param docblock alignment for row_actions (\WP_Post over-padded by one). - WPCS: $label assignment not aligned with the surrounding $visible/$url block. - PHPStan (level 8): `$post instanceof \WP_Post` is always true — the post_row_actions filter types $post as WP_Post — so the redundant guard is dropped, keeping only the post-type fence. Behaviour unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * feat(templates): edit-screen HTML metabox + editor "Save as model" (#865) PR B follow-ups completing the #865 management UI. - CertTemplateAdminScreen: HTML-body + visibility metabox on the ffc_cert_template edit screen (add_meta_boxes_/save_post_ hooks). The CPT only supports 'title', so the template body needs its own field; the save handler guards autosave, verifies a nonce, checks ffc_manage_forms, and sanitizes the body through the HtmlPolicy allowlist (same as the form-layout save) before CertTemplateWriter::update_html + set_visibility. - FormEditor::ajax_save_template (new ffc_save_template AJAX): stores the current layout HTML as a new pool template via CertTemplateWriter::create, under the existing ffc_admin_pdf_nonce + ffc_manage_forms gate, HtmlPolicy-sanitized. - FormEditorLayoutMetabox: "Save as model" button in the Actions row. - ffc-admin-pdf.js: button handler prompts for a name, flushes CodeMirror, and POSTs ffc_save_template; two new localized strings (saveModelPrompt, templateSaved). Re-minified. Tests: FormEditorTest (ajax_save_template success/denied/empty-title), CertTemplateAdminScreenTest (add/render/save metabox paths), admin-pdf.test.js (save-as-model POST + cancel), FormEditorLayoutMetaboxTest (button present). CHANGELOG entry extended. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 --------- Co-authored-by: Claude <noreply@anthropic.com> * feat(templates): relocate default certificate images to assets/ (#865) (#871) #865 crit #7: the shipped background + signature images the default certificate templates use move from the update-fragile html/ folder to the versioned assets/img/certificate-defaults/ (survives updates; a step toward retiring html/). The seed templates (templates/certificate-defaults/*.html) now reference them under assets/. Non-breaking: the images are COPIED, not moved — the html/ originals stay in place through the deprecation window, so existing installs whose seeded/user templates still reference html/*.png keep working until the Phase 3 migration. The instance-specific logos (Logo_Cidade_SP, Logo_DRE — already .distignore'd from the release zip) are intentionally NOT relocated; they become the configurable {{logo_gov}}/{{logo_org}} branding tokens next (Phase 2). Adds a seeder guard test asserting the seed HTML references assets/ and never plugins/ffcertificate/html/. Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-authored-by: Claude <noreply@anthropic.com> * fix(templates): enforce read-only default templates server-side (#865) (#870) * fix(templates): enforce read-only default templates server-side (#865) Closes a deviation from #865 decision #11: the #869 edit metabox let an operator edit/rename/delete a shipped default template, but defaults must be read-only — customizing one means Duplicate → an editable user template. - save_edit_metabox: never rewrite a default's HTML (skips update_html; the textarea is also rendered `readonly` with an explanatory notice). Visibility stays togglable for defaults (a default can be hidden, not edited). - protect_default_caps (map_meta_cap, priority 20 so it runs after CptCapPolicy's map at 10): returns do_not_allow for delete_post on a default — a real server-side block, not just hiding the Trash/Delete row action. - preserve_default_title (wp_insert_post_data): restores a default's shipped title on save, blocking rename. Tests cover all three guards plus the readonly render. User templates are unaffected (edit/rename/delete as before). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * fix(templates): avoid casting get_post_field to string in preserve_default_title (#865) PHPStan level 8: get_post_field() is typed array<int>|int|string, so the blind (string) cast could stringify an array. Guard with is_string() instead — functionally identical for post_title (always a string), PHPStan-clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * test(templates): stub get_post_meta in the pre-existing metabox-save test (#865) The #870 read-only guard added a CertTemplateReader::is_default() call (→ get_post_meta) at the top of save_edit_metabox, so the pre-existing test_save_edit_metabox_persists_html_and_visibility (from #869) now needs get_post_meta mocked. Return '' (a non-default template) so the HTML update still runs and the assertion holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 --------- Co-authored-by: Claude <noreply@anthropic.com> * feat(templates): branding logo tokens {{logo_gov}}/{{logo_org}} (#865 Phase 2) (#872) * feat(templates): branding logo tokens {{logo_gov}}/{{logo_org}} (#865 Phase 2) De-hardcodes the instance-specific logos from the default ficha/receipt templates behind two configurable, site-level branding tokens. - BrandingTokens (new Core helper): resolves {{logo_gov}}/{{logo_org}} to the Settings→General URL, or a generic shipped placeholder (assets/img/certificate-defaults/logo-placeholder.svg) when unset. One resolver + fallback shared by both PDF renderers. - Renderer injection: PdfHtmlRenderer::generate_html (covers certificates AND appointment receipts, which route through it) and the reregistration FichaGenerator both inject logo_gov/logo_org as bare URLs into their token pass (author owns the <img>). - Settings→General "Branding": two logo URL fields with a Media Library picker (new generic data-ffc-media-target handler ffc-branding-media.js, enqueued on the settings page; esc_url_raw on save). - Default html/default_ficha_template.html + html/default_appointment_receipt_1.html now use {{logo_gov}}/{{logo_org}} instead of the .distignore'd SP-specific Logo_*.png. Documented in the token reference. Tests: BrandingTokensTest (configured/fallback/blank), SettingsSaveHandlerTest (esc_url_raw), branding-media.test.js (picker + clear). CHANGELOG under Added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 * fix(templates): CI — module boundary + test stubs for branding tokens (#865) Four CI failures from the Phase 2 branding-tokens PR: - ModuleBoundaryTest: the BrandingTokens (Core) docblock referenced \FreeFormCertificate\Generators\PdfHtmlRenderer, which the boundary scan reads as a new Core→Generators edge. Reworded the docblock to drop the FQCN (the only real cross-module ref is Core→Settings, already in the baseline). - FichaGeneratorTest (2 tests): FichaGenerator::generate now resolves the logo tokens via BrandingTokens → SettingsReader → get_option(); stub get_option in setUp (returns configured logos, so no FFC_PLUGIN_URL fallback needed). - SettingsSaveHandlerTest: the new branding-logo test is the first to exercise esc_url_raw in the general save path; stub it in setUp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 --------- Co-authored-by: Claude <noreply@anthropic.com> * refactor(templates): relocate ficha & receipt defaults out of html/ (#865) (#873) Moves the two document templates out of the update-fragile html/ folder to the versioned templates/documents/, continuing to empty html/ of shipped assets ahead of retiring it (only the legacy html/default_certificate_* glob fallback remains there now). - git mv html/default_ficha_template.html + html/default_appointment_receipt_1.html → templates/documents/. - FichaGenerator::load_template and PdfHtmlRenderer::get_appointment_receipt_template now read from templates/documents/. Both are read fresh from FFC_PLUGIN_DIR at render time (no DB-saved path), so the file+code move is atomic and safe. - feature-ficha.php docs path + the .distignore comment updated. Test: FichaGeneratorTest guards the relocation (files exist at the new path, gone from html/). CHANGELOG under Changed. Claude-Session: https://claude.ai/code/session_01JgLreB3t94fZd6TsH6NSa3 Co-authored-by: Claude <noreply@anthropic.com> * feat(migrations): import legacy html/ certificate templates into the pool (#865) (#875) Adds an "Import Legacy Certificate Templates" migration card (Settings → Migrations) that lifts certificate layouts a site left in the plugin's html/ drop-folder into the ffc_cert_template pool as user templates, advancing the retirement of the update-fragile html/ folder. - ImportLegacyTemplatesMigrationStrategy: scans html/*.html for basenames containing "certificate" (the #443 convention), excludes the three shipped defaults (already seeded from templates/certificate-defaults/), and imports each via CertTemplateWriter::create(). Non-destructive and idempotent — every handled basename is recorded in the ffc_imported_legacy_templates option, and a file that fails to import is still marked handled so the batch runner terminates on is_complete. Legacy dir is constructor-injectable for tests. - Registry entry + status-calculator switch case wire the card. Auth is inherited from MigrationActionsAjaxEndpoint (ffc_manage_settings_dangerzone). - Adds the Migrations>Admin edge to the module-boundary baseline (a migration touching the Admin-owned pool, mirroring Migrations>Reregistration). - Tests co…
Bumps yoast/phpunit-polyfills from 2.0.5 to 4.0.0.
Release notes
Sourced from yoast/phpunit-polyfills's releases.
... (truncated)
Changelog
Sourced from yoast/phpunit-polyfills's changelog.
... (truncated)
Commits
134921bMerge pull request #254 from Yoast/feature/4.x/changelog-v4.0.0467709fChangelog for the 4.0.0 release and other documentation updatesb7d234fMerge branch '3.x' into 4.x9cf2ccdMerge pull request #253 from Yoast/feature/3.x/changelog-v3.1.259bd807Changelog for the 3.1.2 release083d158Merge branch '3.x' into 4.x615cd5bMerge branch '2.x' into 3.x93c9b2dMerge branch '3.x' into 4.x47d6fb2Merge branch '2.x' into 3.xd2874f0Changelog for the 1.1.4 release