PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.1.6
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.1.6
5.6.2 5.6.3 5.6.1 5.6.0 5.5.0 5.4.0 5.3.2 5.3.1 5.1.6 5.1.5 trunk 2.1.5 2.11 2.12 2.13 2.15 3.0.0 3.0.1 3.0.2 3.0.3 3.0.5 3.0.51 3.0.60 3.0.61 3.0.62 All 38 releases
double-opt-in / src / Migration / MigrationFormCompletenessSweep.php

MigrationFormCompletenessSweep.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.1.6, at src/Migration/MigrationFormCompletenessSweep.php

151 lines 4.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Migration: Form Completeness Sweep
4 *
5 * @package Forge12\DoubleOptIn\Migration
6 * @since 4.5.0
7 */
8
9 namespace Forge12\DoubleOptIn\Migration;
10
11 use Forge12\DoubleOptIn\FormSettings\FormSettingsDTO;
12 use Forge12\DoubleOptIn\FormSettings\FormSettingsService;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 /**
19 * One-shot sweep that auto-disables every form whose DOI configuration
20 * is enabled-but-incomplete at the moment this migration runs.
21 *
22 * Background (plan/doi-completeness-gate.md §2.4): the completeness-gate
23 * in the REST controllers (§2.2 + §2.3) prevents NEW forms from entering
24 * the broken state, but sites that have been running the plugin for a
25 * while may have existing forms that became incomplete over time (a
26 * form field was deleted, a custom template was removed, etc.). This
27 * migration catches them once at upgrade time.
28 *
29 * What it does:
30 * - Walks every post_meta row with key `f12-cf7-doubleoptin` (the
31 * storage key {@see FormSettingsService::META_KEY}).
32 * - Skips already-disabled forms — they are by definition not broken
33 * in the user-visible sense.
34 * - For each enabled form, builds a {@see FormSettingsDTO} from the
35 * stored array and calls {@see FormSettingsDTO::getMissingRequiredFields()}.
36 * - When non-empty, flips `enable` to 0 in-place and fires
37 * `f12_doi_form_auto_disabled_incomplete` so listeners (e.g. the
38 * AdminNoticeIncompleteForms service) can surface the change to the
39 * site admin.
40 * - Stores the affected-forms list in
41 * `f12_doi_completeness_sweep_affected` so the admin notice has a
42 * rendering source.
43 *
44 * Idempotency: the {@see MigrationRegistry} guarantees this `up()`
45 * method runs exactly once per site via the
46 * `f12_doi_applied_migrations` option. A second run would simply find
47 * no enabled-incomplete forms (since the first pass disabled them) and
48 * be a no-op, but the registry skips it entirely.
49 *
50 * @internal Not part of any public API. Migration logic only.
51 */
52 class MigrationFormCompletenessSweep implements MigrationInterface {
53
54 /**
55 * Option key used by the admin-notice surface to read the list of
56 * forms that this migration auto-disabled. Stays around until the
57 * admin explicitly dismisses the notice.
58 */
59 public const AFFECTED_FORMS_OPTION = 'f12_doi_completeness_sweep_affected';
60
61 /**
62 * {@inheritdoc}
63 */
64 public function getId(): string {
65 return 'core_20260512_form_completeness_sweep';
66 }
67
68 /**
69 * {@inheritdoc}
70 */
71 public function getDescription(): string {
72 return 'Auto-disable existing DOI forms whose configuration is incomplete (see plan/doi-completeness-gate.md §2.4).';
73 }
74
75 /**
76 * {@inheritdoc}
77 */
78 public function up( \wpdb $wpdb ): void {
79 $metaKey = FormSettingsService::META_KEY;
80
81 $rows = $wpdb->get_results(
82 $wpdb->prepare(
83 "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s",
84 $metaKey
85 )
86 );
87
88 if ( empty( $rows ) ) {
89 return;
90 }
91
92 $affected = array();
93
94 foreach ( $rows as $row ) {
95 $data = maybe_unserialize( $row->meta_value );
96 if ( ! is_array( $data ) ) {
97 continue;
98 }
99
100 // Skip already-disabled forms — they cannot be the
101 // "enabled-but-broken" bug-state by definition.
102 $isEnabled = ! empty( $data['enable'] ) || ! empty( $data['enabled'] );
103 if ( ! $isEnabled ) {
104 continue;
105 }
106
107 $dto = FormSettingsDTO::fromArray( $data );
108 $missing = $dto->getMissingRequiredFields();
109 if ( empty( $missing ) ) {
110 continue;
111 }
112
113 // Flip the stored value to disabled. We mutate the stored
114 // array directly rather than re-serialising the DTO so
115 // extension-data fields contributed by addons via the
116 // f12_doi_settings_dto_* filters survive untouched.
117 $data['enable'] = 0;
118 $data['enabled'] = false;
119 update_post_meta( (int) $row->post_id, $metaKey, $data );
120
121 $affected[] = array(
122 'form_id' => (int) $row->post_id,
123 'missing' => array_values( $missing ),
124 );
125
126 /**
127 * Fired by the completeness-sweep migration AND by the
128 * controller-side auto-disable path (plan §2.2). Listeners
129 * should be additive and tolerate multiple firings for the
130 * same form_id over time.
131 *
132 * @since 4.5.0
133 *
134 * @param string $formId Form ID as a string (composite IDs
135 * for Elementor — the migration always
136 * passes the bare post_id since that
137 * is the storage key).
138 * @param array $missing Stable required-field IDs.
139 */
140 do_action( 'f12_doi_form_auto_disabled_incomplete', (string) $row->post_id, $missing );
141 }
142
143 if ( ! empty( $affected ) ) {
144 // Persist the list so the admin-notice surface can render
145 // it on the next admin page-load. Cleared on dismiss
146 // (handled by AdminNoticeIncompleteForms).
147 update_option( self::AFFECTED_FORMS_OPTION, $affected, false );
148 }
149 }
150 }
151