| 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 |
|