PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.8
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.8
2.12.8 2.12.7 2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 All 98 releases
← All changes | inc/database/register.php +247 -2 0.0.13 → 2.12.8 View file →
@@ -10,8 +10,10 @@
10 10
11 11 namespace SRFM\Inc\Database;
12 12
13 13 use SRFM\Inc\Database\Tables\Entries;
14 +use SRFM\Inc\Database\Tables\Payments;
15 +use SRFM\Inc\Helper;
14 16
15 17 // Exit if accessed directly.
16 18 defined( 'ABSPATH' ) || exit;
17 19
@@ -20,10 +22,32 @@
20 22 *
21 23 * @since 0.0.13
22 24 */
23 25 class Register {
26 + /**
27 + * Transient caching a healthy entries-table check.
28 + *
29 + * Only the healthy answer is ever cached. A missing table is re-queried every
30 + * time, so the warning disappears the moment the table is back rather than
31 + * lingering for the rest of the TTL.
32 + *
33 + * @since 2.12.6
34 + */
35 + public const ENTRIES_TABLE_CHECK_TRANSIENT = 'srfm_entries_table_present';
24 36
25 37 /**
38 + * Per-request memo for the entries-table check.
39 + *
40 + * The check is consulted from three places in one admin page load (notice
41 + * registration, notice rendering, analytics), and this keeps that to a single
42 + * transient read.
43 + *
44 + * @var bool|null
45 + * @since 2.12.6
46 + */
47 + private static $entries_table_present = null;
48 +
49 + /**
26 50 * Init database registration.
27 51 *
28 52 * @since 0.0.13
29 53 * @return void
@@ -35,10 +59,13 @@
35 59 * 2. Init create method, which create the table if the table does not exists.
36 60 * 3. Init maybe_add_new_columns, it only runs if we have new columns definition and DB is upgradable ( has new version ).
37 61 * 4. Init maybe_rename_columns, it only runs if got any columns to rename and DB is upgradable ( has new version ).
38 62 * 5. Finally, stop the DB upgrade and update the current version in option table.
63 + *
64 + * Replaced self::get_db_tables() to static::get_db_tables() for allowing overrides.
65 + * @since 1.13.0
39 66 */
40 - foreach ( self::get_db_tables() as $instance ) {
67 + foreach ( static::get_db_tables() as $instance ) {
41 68 $instance->start_db_upgrade();
42 69
43 70 if ( $instance->is_db_upgradable() ) {
44 71 // Only execute below methods if DB is upgradable.
@@ -59,8 +86,226 @@
59 86 * @return array<string,\SRFM\Inc\Database\Base>
60 87 */
61 88 public static function get_db_tables() {
62 89 return [
63 - 'entries' => Entries::get_instance(),
90 + 'entries' => Entries::get_instance(),
91 + 'payments' => Payments::get_instance(),
64 92 ];
93 + }
94 +
95 + /**
96 + * Whether the entries table is missing from the database.
97 + *
98 + * A dropped table is never recreated on its own: `srfm_database_table_versions`
99 + * still records the current version, so start_db_upgrade() marks the table
100 + * non-upgradable and create() early-returns. Nothing else in the plugin notices,
101 + * and every submission silently fails to save. This is what surfaces that.
102 + *
103 + * Returns false while the stored version is absent — Register::init() is going
104 + * to create the table on this very request, so reporting it missing would show
105 + * a "your database needs updating" notice during a perfectly normal install.
106 + *
107 + * @param bool $force Skip both caches and re-query.
108 + * @since 2.12.6
109 + * @return bool
110 + */
111 + public static function is_entries_table_missing( $force = false ) {
112 + if ( ! $force && null !== self::$entries_table_present ) {
113 + return ! self::$entries_table_present;
114 + }
115 +
116 + if ( ! $force && get_transient( self::ENTRIES_TABLE_CHECK_TRANSIENT ) ) {
117 + self::$entries_table_present = true;
118 + return false;
119 + }
120 +
121 + $versions = Helper::get_array_value( get_option( 'srfm_database_table_versions', [] ) );
122 +
123 + // No recorded version means this is a fresh install or an upgrade in
124 + // progress; init() creates the table this request. Nothing to warn about.
125 + if ( empty( $versions['entries'] ) ) {
126 + self::$entries_table_present = true;
127 + return false;
128 + }
129 +
130 + $present = Entries::get_instance()->table_exists();
131 +
132 + // Cache only the healthy answer. See the transient's docblock.
133 + if ( $present ) {
134 + set_transient( self::ENTRIES_TABLE_CHECK_TRANSIENT, 1, DAY_IN_SECONDS );
135 + // Backfill the owner signature once, so a table created before this plugin
136 + // wrote signatures can still be proven ours if the prefix later changes.
137 + if ( ! get_option( 'srfm_entries_owner_stamped' ) ) {
138 + Entries::get_instance()->stamp_owner_signature();
139 + update_option( 'srfm_entries_owner_stamped', 1, false );
140 + }
141 + } else {
142 + delete_transient( self::ENTRIES_TABLE_CHECK_TRANSIENT );
143 + }
144 +
145 + self::$entries_table_present = $present;
146 +
147 + return ! $present;
148 + }
149 +
150 + /**
151 + * Recreate the entries table when it is missing.
152 + *
153 + * Clearing the stored version is the whole repair: start_db_upgrade() then
154 + * treats the table as new, and init() runs the same ordered steps it runs on a
155 + * fresh install. Reusing init() rather than re-implementing those steps is
156 + * deliberate — the repair path cannot drift from the install path.
157 + *
158 + * Only the `entries` key is removed. The option is shared with the payments
159 + * table and, when Pro is active, three more; clearing the whole option would
160 + * force an unrelated re-upgrade pass across all of them from free-plugin code.
161 + *
162 + * The result is a fresh existence check, never create()'s return value. A
163 + * repair that reported success on the strength of create() alone could record
164 + * the version again while the table was still missing — which would hide the
165 + * problem permanently and be worse than doing nothing.
166 + *
167 + * @since 2.12.6
168 + * @return bool True when the table exists afterwards.
169 + */
170 + public static function repair_entries_table() {
171 + $entries = Entries::get_instance();
172 +
173 + // Prefer adopting the site's own data over manufacturing an empty table.
174 + // A changed table prefix leaves the rows behind under the old name, and
175 + // creating a fresh table would strand every stored entry while looking like
176 + // a successful repair.
177 + $adoptable = $entries->find_adoptable_table();
178 +
179 + if ( '' !== $adoptable ) {
180 + if ( $entries->table_belongs_to_site( $adoptable ) ) {
181 + // Provenance confirms this site owns the table: safe to rename it in.
182 + if ( $entries->adopt_table( $adoptable ) ) {
183 + self::run_entries_schema_catchup();
184 + delete_option( 'srfm_entries_unrecovered' );
185 + return ! self::is_entries_table_missing( true );
186 + }
187 +
188 + // The table is ours but the rename failed — e.g. the DB user lacks the
189 + // privilege RENAME needs, or a metadata-lock timeout. Manufacturing an
190 + // empty table over recoverable data and reporting success would be worse
191 + // than doing nothing, so refuse and leave a durable trail for support.
192 + self::record_unrecovered_entries_table( $adoptable, 'adopt_failed' );
193 + return false;
194 + }
195 +
196 + // A single same-schema candidate exists but its ownership cannot be
197 + // proven. On shared hosting it may belong to an unrelated install, and
198 + // renaming it in would destroy that site's data. Leave it untouched,
199 + // record it so it can be recovered by hand, then fall back to a fresh
200 + // table below.
201 + self::record_unrecovered_entries_table( $adoptable, 'unverified' );
202 + }
203 +
204 + $versions = Helper::get_array_value( get_option( 'srfm_database_table_versions', [] ) );
205 +
206 + unset( $versions['entries'] );
207 + update_option( 'srfm_database_table_versions', $versions );
208 +
209 + static::init();
210 +
211 + return ! self::is_entries_table_missing( true );
212 + }
213 +
214 + /**
215 + * Drop the cached entries-table detection so the next check re-queries.
216 + *
217 + * Called when a live entries write fails: a table cached as present earlier in
218 + * the day may have just been dropped, and trusting the stale healthy answer
219 + * would hide the notice for up to the transient's lifetime.
220 + *
221 + * @since 2.12.6
222 + * @return void
223 + */
224 + public static function flush_entries_table_cache() {
225 + self::$entries_table_present = null;
226 + delete_transient( self::ENTRIES_TABLE_CHECK_TRANSIENT );
227 + }
228 +
229 + /**
230 + * The entries table sitting under a different prefix, if there is one.
231 + *
232 + * Exposed so the notice can say whether the repair will bring existing entries
233 + * back with it or start from empty — the difference matters a great deal to
234 + * whoever is about to click the button.
235 + *
236 + * @since 2.12.6
237 + * @return string Full table name, or '' when there is nothing to adopt.
238 + */
239 + public static function get_adoptable_entries_table() {
240 + if ( ! self::is_entries_table_missing() ) {
241 + return '';
242 + }
243 +
244 + $entries = Entries::get_instance();
245 + $candidate = $entries->find_adoptable_table();
246 +
247 + // Only report a candidate the repair would actually adopt — one we can prove
248 + // belongs to this site. An unverifiable table is left untouched and a fresh
249 + // table created instead, so the notice must not promise to keep its entries.
250 + if ( '' === $candidate || ! $entries->table_belongs_to_site( $candidate ) ) {
251 + return '';
252 + }
253 +
254 + return $candidate;
255 + }
256 +
257 + /**
258 + * Re-run the install-time schema steps against a just-adopted table.
259 + *
260 + * An adopted table can carry an older revision of this plugin's schema (a
261 + * superset of column names still passes the adoption guard), so reuse the same
262 + * ordered create/add-column/rename pass the install path runs rather than
263 + * assuming the adopted schema is already current. create() is CREATE TABLE IF
264 + * NOT EXISTS, so it is a no-op on the table that is now in place.
265 + *
266 + * @since 2.12.6
267 + * @return void
268 + */
269 + private static function run_entries_schema_catchup() {
270 + $versions = Helper::get_array_value( get_option( 'srfm_database_table_versions', [] ) );
271 +
272 + unset( $versions['entries'] );
273 + update_option( 'srfm_database_table_versions', $versions );
274 +
275 + static::init();
276 + }
277 +
278 + /**
279 + * Record a table that holds recoverable entries but was not adopted.
280 + *
281 + * Fired when a candidate is found that either cannot be proven to belong to
282 + * this site or could not be renamed in. Persists the name and reason so a
283 + * support engineer can find and reconnect it by hand, rather than letting it
284 + * be discarded silently the moment a fresh empty table exists.
285 + *
286 + * @param string $table Full name of the table left in place.
287 + * @param string $reason 'unverified' or 'adopt_failed'.
288 + * @since 2.12.6
289 + * @return void
290 + */
291 + private static function record_unrecovered_entries_table( $table, $reason ) {
292 + update_option(
293 + 'srfm_entries_unrecovered',
294 + [
295 + 'table' => $table,
296 + 'reason' => $reason,
297 + 'time' => time(),
298 + ],
299 + false
300 + );
301 +
302 + /**
303 + * Fires when entries data was found but not automatically recovered.
304 + *
305 + * @param string $table Full name of the table left in place.
306 + * @param string $reason 'unverified' or 'adopt_failed'.
307 + * @since 2.12.6
308 + */
309 + do_action( 'srfm_entries_table_unrecovered', $table, $reason );
65 310 }
66 311 }