PluginProbe
404 Solution / 4.2.0
404 Solution v4.2.0
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / DatabaseUpgradesEtcTrait_PluginUpdate.php

DatabaseUpgradesEtcTrait_PluginUpdate.php in 404 Solution 4.2.0, at includes/DatabaseUpgradesEtcTrait_PluginUpdate.php

256 lines 10.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 trait ABJ_404_Solution_DatabaseUpgradesEtc_PluginUpdateTrait {
8
9
10 /**
11 * Migrate existing redirects from absolute paths to relative paths.
12 * This is a one-time migration for upgrading from versions prior to 2.37.0.
13 * Fixes Issue #24: Redirects now survive WordPress subdirectory changes.
14 *
15 * Uses a single atomic SQL UPDATE statement - no locks or transactions needed.
16 *
17 * @return array<string, mixed> Migration results with counts
18 */
19 function migrateURLsToRelativePaths() {
20 global $wpdb;
21
22 $abj404logging = abj_service('logging');
23
24 // Get current WordPress subdirectory
25 $homeURL = get_home_url();
26 $urlPath = parse_url($homeURL, PHP_URL_PATH);
27
28 if ($urlPath === false || $urlPath === null) {
29 $urlPath = '';
30 }
31
32 $decodedPath = rawurldecode(rtrim($urlPath, '/'));
33 $subdirectory = preg_replace('/[\x00-\x1F\x7F]/', '', $decodedPath);
34
35 $results = array(
36 'redirects_updated' => 0,
37 'subdirectory' => $subdirectory,
38 'errors' => array()
39 );
40
41 // Skip if WordPress is at domain root (no subdirectory)
42 if (empty($subdirectory) || $subdirectory === '/') {
43 $abj404logging->debugMessage("No subdirectory detected. Migration skipped.");
44 return $results;
45 }
46
47 $startTime = microtime(true);
48 $redirectsTable = $this->dbCore->getPrefixedTableName('abj404_redirects');
49
50 $abj404logging->infoMessage("Migrating redirects table to relative paths...");
51
52 // Single SQL UPDATE - atomic at database level, no transaction needed
53 // Uses CHAR_LENGTH() for UTF-8 multibyte character safety
54 $subdirectoryWithSlash = $subdirectory . '/';
55
56 // canonical_url stays in lockstep with url — same CASE expression
57 // applied to both columns so the captured-page JOIN keeps matching
58 // logs_hits.requested_url after the path rewrite. Wrapped in
59 // CONCAT('/', TRIM(BOTH '/' FROM ...)) to preserve the canonical form
60 // (no leading-slash duplication, no trailing slash). The CASE WHEN
61 // canonical_url IS NOT NULL guard avoids overwriting NULL rows
62 // (post-upgrade, pre-backfill) which the daily backfill will fill.
63 $canonicalCase = "CASE
64 WHEN url = %s OR url = %s THEN '/'
65 WHEN url LIKE %s THEN CONCAT('/', SUBSTRING(url, CHAR_LENGTH(%s) + 1))
66 ELSE url
67 END";
68
69 $updateQuery = $wpdb->prepare(
70 "UPDATE {$redirectsTable}
71 SET url = " . $canonicalCase . ",
72 canonical_url = CASE
73 WHEN canonical_url IS NULL THEN NULL
74 ELSE CONCAT('/', TRIM(BOTH '/' FROM (" . $canonicalCase . ")))
75 END
76 WHERE url = %s OR url = %s OR url LIKE %s",
77 $subdirectory, // url CASE: exact match /blog
78 $subdirectoryWithSlash, // url CASE: with slash /blog/
79 $wpdb->esc_like($subdirectoryWithSlash) . '%', // url CASE: with path /blog/*
80 $subdirectoryWithSlash, // url SUBSTRING length
81 $subdirectory, // canonical CASE: exact match /blog
82 $subdirectoryWithSlash, // canonical CASE: with slash /blog/
83 $wpdb->esc_like($subdirectoryWithSlash) . '%', // canonical CASE: with path /blog/*
84 $subdirectoryWithSlash, // canonical SUBSTRING length
85 $subdirectory, // WHERE: exact match
86 $subdirectoryWithSlash, // WHERE: with slash
87 $wpdb->esc_like($subdirectoryWithSlash) . '%' // WHERE: with path
88 );
89
90 // DAO-bypass-approved: One-shot path-relativization migration; already prepared (multi-line CASE with prefix args), wpdb->query for rows-affected return; tightly mocked in DatabaseMigrationTest
91 $updateResult = $wpdb->query($updateQuery);
92
93 // Check for errors
94 if ($updateResult === false) {
95 $results['errors'][] = "Failed to update redirects: " . $wpdb->last_error;
96 if (!$this->dbCore->classifyAndHandleInfrastructureError($wpdb->last_error ?? '')) {
97 $abj404logging->errorMessage("Migration failed: " . $wpdb->last_error);
98 }
99 } else {
100 $results['redirects_updated'] = $updateResult;
101
102 $duration = microtime(true) - $startTime;
103 $abj404logging->infoMessage(sprintf(
104 "Migrated %d redirects in %.4f seconds.",
105 $results['redirects_updated'],
106 $duration
107 ));
108
109 // Note: Log entries are intentionally NOT migrated for performance.
110 // Historical logs with absolute paths are display-only and don't affect functionality.
111
112 // Mark migration as complete
113 update_option('abj404_migrated_to_relative_paths', '1');
114 update_option('abj404_migration_results', $results);
115 $abj404logging->infoMessage("Migration to relative paths completed successfully.");
116 }
117
118 return $results;
119 }
120
121
122 /** @return void */
123 function updatePluginCheck() {
124
125 $pluginInfo = $this->dao->getLatestPluginVersion();
126
127 $shouldUpdate = $this->shouldUpdate($pluginInfo);
128
129 if ($shouldUpdate) {
130 $this->doUpdatePlugin($pluginInfo);
131 }
132 }
133
134 /**
135 * @param array<string, mixed> $pluginInfo
136 * @return void
137 */
138 function doUpdatePlugin($pluginInfo) {
139
140 $this->logger->infoMessage("Attempting update to " . $pluginInfo['version']);
141
142 // do the update.
143 if (!class_exists('WP_Upgrader')) {
144 $this->logger->infoMessage("Including WP_Upgrader for update.");
145 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
146 }
147 if (!class_exists('Plugin_Upgrader')) {
148 $this->logger->infoMessage("Including Plugin_Upgrader for update.");
149 require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';
150 }
151 if (!function_exists('show_message')) {
152 $this->logger->infoMessage("Including misc.php for update.");
153 require_once ABSPATH . 'wp-admin/includes/misc.php';
154 }
155 if (!class_exists('Plugin_Upgrader')) {
156 $this->logger->warn("There was an issue including the Plugin_Upgrader class.");
157 return;
158 }
159 if (!function_exists('show_message')) {
160 $this->logger->warn("There was an issue including the misc.php class.");
161 return;
162 }
163
164 $this->logger->infoMessage("Includes for update complete. Updating... ");
165
166 ob_start();
167 $upgrader = new Plugin_Upgrader();
168 $upret = $upgrader->upgrade(ABJ404_SOLUTION_BASENAME);
169 if ($upret) {
170 $this->logger->infoMessage("Plugin successfully upgraded to " . $pluginInfo['version']);
171 }
172 $output = "";
173 if (@ob_get_contents()) {
174 $output = @ob_get_contents();
175 @ob_end_clean();
176 }
177 if ($this->f->strlen(trim($output)) > 0) {
178 $this->logger->infoMessage("Upgrade output: " . $output);
179 }
180
181 $activateResult = activate_plugin(ABJ404_NAME);
182 if ($activateResult instanceof WP_Error) {
183 $this->logger->errorMessage("Plugin activation error " .
184 json_encode($activateResult->get_error_codes()) . ": " . json_encode($activateResult->get_error_messages()));
185
186 } else {
187 $this->logger->infoMessage("Successfully reactivated plugin after upgrade to version " .
188 $pluginInfo['version']);
189 }
190 }
191
192 /**
193 * @param array<string, mixed> $pluginInfo
194 * @return bool
195 */
196 function shouldUpdate($pluginInfo) {
197
198
199 $options = $this->logic->getOptions(true);
200 $latestVersion = isset($pluginInfo['version']) && is_string($pluginInfo['version']) ? $pluginInfo['version'] : '';
201
202 if (ABJ404_VERSION == $latestVersion) {
203 $this->logger->debugMessage("The latest plugin version is already installed (" .
204 ABJ404_VERSION . ").");
205 return false;
206 }
207
208 // don't overwrite development versions.
209 if (version_compare(ABJ404_VERSION, $latestVersion) == 1) {
210 $this->logger->infoMessage("Development version: A more recent version is installed than " .
211 "what is available on the WordPress site (" . ABJ404_VERSION . " / " .
212 $latestVersion . ").");
213 return false;
214 }
215
216 $serverName = array_key_exists('SERVER_NAME', $_SERVER) ? $_SERVER['SERVER_NAME'] : (array_key_exists('HTTP_HOST', $_SERVER) ? $_SERVER['HTTP_HOST'] : '(not found)');
217 if (in_array($serverName, array('127.0.0.1', '::1', 'localhost'))) {
218 $this->logger->infoMessage("Update narrowly avoided on localhost.");
219 return false;
220 }
221
222 // 1.12.0 becomes array("1", "12", "0")
223 $myVersionArray = explode(".", ABJ404_VERSION);
224 $latestVersionArray = explode(".", $latestVersion);
225
226 // check the latest date to see if it's been long enough to update.
227 $lastUpdated = isset($pluginInfo['last_updated']) && is_string($pluginInfo['last_updated']) ? $pluginInfo['last_updated'] : '';
228 $lastReleaseDate = new DateTime($lastUpdated);
229 $todayDate = new DateTime();
230 $dateInterval = $lastReleaseDate->diff($todayDate);
231 $daysDifference = $dateInterval->days;
232
233 // if there's a new minor version then update.
234 // only update if it was released at least 3 days ago.
235 if ($myVersionArray[0] == $latestVersionArray[0] &&
236 $myVersionArray[1] == $latestVersionArray[1] &&
237 intval($myVersionArray[2]) < intval($latestVersionArray[2]) &&
238 $daysDifference >= 3) {
239
240 $this->logger->infoMessage("A new minor version is available (" .
241 $latestVersion . "), currently version " . ABJ404_VERSION . " is installed.");
242 return true;
243 }
244
245 $minDaysDifference = $options['days_wait_before_major_update'];
246 if ($daysDifference >= $minDaysDifference) {
247 $this->logger->infoMessage("The latest major version is old enough for updating automatically (" .
248 $minDaysDifference . "days minimum, version " . $latestVersion . " is " . $daysDifference .
249 " days old), currently version " . ABJ404_VERSION . " is installed.");
250 return true;
251 }
252
253 return false;
254 }
255 }
256