PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 2.0.11 All 82 releases
yatra / app / Upgrades / FreeUpgradeRunner.php

FreeUpgradeRunner.php in Yatra – Travel Booking & Tour Operator Software trunk, at app/Upgrades/FreeUpgradeRunner.php

190 lines 7.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Upgrades;
6
7 use Yatra\Core\Database;
8 use Yatra\Services\InstallerService;
9 use Yatra\Upgrades\Versions\Upgrade_3_0_5;
10
11 /**
12 * Orchestrates Yatra Free upgrades: {@see Database::createTables()} for schema sync, then version-gated
13 * {@see Versions\* } steps (ALTER / DROP / data) that do not run on a fresh install at the current version.
14 *
15 * Hooks:
16 * - admin_init (priority 5): delta upgrades + version option bump + idempotent maintenance.
17 *
18 * Extension points:
19 * - {@see 'yatra_free_upgraded'} — after yatra_version was bumped. Args: (string $from, string $to).
20 * - {@see 'yatra_free_upgrades_ran'} — every admin_init after work. Args: (string $from, string $to, bool $bumped).
21 */
22 final class FreeUpgradeRunner
23 {
24 public const VERSION_OPTION = 'yatra_version';
25
26 /** Request-level short-circuit so we never re-enter the rename path in
27 * the same PHP process once it has been settled (either completed,
28 * skipped because the option flag is set, or back-off-throttled). */
29 private static bool $renameSettledThisRequest = false;
30
31 public static function register(): void
32 {
33 add_action('admin_init', [self::class, 'runAdminUpgrades'], 5);
34
35 // Frontend + admin: triggers the one-shot `wp_yatra_new_*` ->
36 // `wp_yatra_*` rename without waiting for an admin pageview.
37 // Heavily short-circuited — see runEarlyRenameHeal() — so the
38 // steady-state cost per request is one boolean check.
39 add_action('init', [self::class, 'runEarlyRenameHeal'], 0);
40 }
41
42 /**
43 * Cheap-path entry for the one-shot table-rename. Layered guards keep
44 * the cost minimal on the common case where the rename is already done:
45 *
46 * 1. Request-level static — first call sets it, subsequent calls in
47 * the same PHP process exit immediately.
48 * 2. Autoloaded option flag — WordPress preloads all autoload=yes
49 * options on boot, so `get_option()` is an array lookup, not a
50 * query.
51 * 3. Failure-backoff transient — if a prior attempt failed (e.g.
52 * DB user lacks RENAME privilege), we don't retry for 15 min so
53 * we don't slow every pageview indefinitely.
54 * 4. wp_cache_add concurrency lock — only one request at a time
55 * tries the rename. Others see the lock and skip.
56 *
57 * Anything thrown by the rename step is caught here so a partial
58 * migration / DB hiccup can never fatal the page.
59 */
60 public static function runEarlyRenameHeal(): void
61 {
62 if (self::$renameSettledThisRequest) {
63 return;
64 }
65
66 if (get_option(Upgrade_3_0_5::RENAME_DONE_OPTION)) {
67 self::$renameSettledThisRequest = true;
68 return;
69 }
70
71 // Backoff: if a recent attempt failed, skip until the transient
72 // expires. Prevents persistent failures (permission denied,
73 // disk full) from running on every pageview forever.
74 if (get_transient('yatra_table_rename_backoff_v1')) {
75 self::$renameSettledThisRequest = true;
76 return;
77 }
78
79 // Concurrency lock: ensure only one process attempts the rename
80 // at a time. 30s TTL is the upper bound for a slow rename;
81 // wp_cache_add returns false if the key already exists.
82 if (!wp_cache_add('yatra_table_rename_lock_v1', 1, 'yatra', 30)) {
83 // Another request is renaming — let it finish.
84 self::$renameSettledThisRequest = true;
85 return;
86 }
87
88 try {
89 $ok = Upgrade_3_0_5::runTableRenameOnce();
90 if (!$ok) {
91 // Don't retry on every request — back off for 15 minutes
92 // so a persistent failure doesn't trash site responsiveness.
93 set_transient('yatra_table_rename_backoff_v1', 1, 15 * MINUTE_IN_SECONDS);
94 }
95 } catch (\Throwable $e) {
96 // Defensive: nothing the rename does should reach here, but
97 // never let a migration exception take down a public page.
98 if (function_exists('error_log')) {
99 error_log('[Yatra rename] uncaught: ' . $e->getMessage());
100 }
101 set_transient('yatra_table_rename_backoff_v1', 1, 15 * MINUTE_IN_SECONDS);
102 } finally {
103 wp_cache_delete('yatra_table_rename_lock_v1', 'yatra');
104 self::$renameSettledThisRequest = true;
105 }
106 }
107
108 public static function runAdminUpgrades(): void
109 {
110 if (!defined('YATRA_VERSION')) {
111 return;
112 }
113
114 // *** Rename BEFORE createTables ***
115 // Database::createTables() runs dbDelta, which would create empty
116 // `wp_yatra_*` placeholders alongside the live `wp_yatra_new_*`
117 // tables — triggering Upgrade_3_0_5's both-exist branch on the
118 // very next call. Doing the rename first means dbDelta sees the
119 // canonical names already in place and is a no-op for them.
120 // runEarlyRenameHeal() is itself heavily short-circuited and
121 // safe to call from both init and admin_init.
122 self::runEarlyRenameHeal();
123
124 $to = YATRA_VERSION;
125 $stored = get_option(self::VERSION_OPTION, false);
126
127 if ($stored === false) {
128 add_option(self::VERSION_OPTION, $to);
129 self::runIdempotentMaintenance();
130 do_action('yatra_free_upgrades_ran', '0.0.0', $to, false);
131
132 return;
133 }
134
135 $from = (string) $stored;
136
137 if (version_compare($from, $to, '>=')) {
138 self::runIdempotentMaintenance();
139 do_action('yatra_free_upgrades_ran', $from, $to, false);
140
141 return;
142 }
143
144 Database::createTables();
145
146 foreach (FreeUpgradeRegistry::stepsForHook('admin_init') as $class) {
147 if ($class::shouldApply($from, $to)) {
148 $class::run($from, $to);
149 }
150 }
151
152 update_option(self::VERSION_OPTION, $to);
153 do_action('yatra_free_upgraded', $from, $to);
154
155 self::runIdempotentMaintenance();
156 do_action('yatra_free_upgrades_ran', $from, $to, true);
157 }
158
159 private static function runIdempotentMaintenance(): void
160 {
161 InstallerService::maybeBackfillEmailTemplateDefaults();
162 InstallerService::maybeNormalizeMigratedCouponDiscountStatuses();
163 // Heal recurring availability rules whose new-schema columns
164 // (rule_type / seats_total / interval_days / interval_start_date)
165 // were never written by sample data or pre-3.x writers. Without this
166 // the new admin UI showed phantom "1 on All & Active" badges for
167 // legacy rows that rendered as broken weekly rules.
168 InstallerService::maybeNormalizeAvailabilityRulesLegacyData();
169 // Widen bookings.status enum to accept 'pending_verification' and
170 // restore rows that earlier inserts coerced to '' under the old
171 // enum. Version-independent (runs on every admin pageview, gated
172 // by its own one-shot option) so installs whose stored version
173 // already moved past 3.0.5 by a failed upgrade attempt still heal.
174 InstallerService::maybeAddPendingVerificationBookingStatus();
175
176 // Add the nullable `duration_hours` column to trips (hour-based tours).
177 // Additive + idempotent; existing trips get NULL and behave unchanged.
178 InstallerService::maybeAddTripDurationHoursColumn();
179
180 // Widen reviews.status enum to accept 'spam' / 'trash' (and
181 // recover rows previously coerced to ''). Called directly here
182 // rather than relying on the version-chain in runAdminUpgrades()
183 // because the bug ships IN 3.0.5 itself — installs whose stored
184 // yatra_version already equals the code version short-circuit
185 // past the chain. The upgrade step's own one-shot option flag
186 // gates the work so this is cheap on subsequent pageviews.
187 Upgrade_3_0_5::run(YATRA_VERSION, YATRA_VERSION);
188 }
189 }
190