PluginProbe
Redirection / 5.9.0
Redirection v5.9.0
5.10.0 5.9.0 5.8.1 5.8.0 3.7.2 3.7.3 4.0 4.0.1 4.1 4.1.1 4.2 4.2.1 4.2.2 4.2.3 4.3 4.3.1 4.3.2 4.3.3 4.4 4.4.1 4.4.2 4.5 4.5.1 4.6.2 4.7.1 All 130 releases
redirection / database / database-status.php

database-status.php in Redirection 5.9.0, at database/database-status.php

570 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @phpstan-type DatabaseStatus array{
5 * status: string|false,
6 * inProgress: bool,
7 * current?: string,
8 * next?: string,
9 * time?: float,
10 * manual?: array<int, string>,
11 * result?: 'ok'|'error',
12 * reason?: string|false,
13 * debug?: array<int, string>,
14 * complete?: int|float
15 * }
16 */
17 class Red_Database_Status {
18 // Used in < 3.7 versions of Redirection, but since migrated to general settings
19 const OLD_DB_VERSION = 'redirection_version';
20 const DB_UPGRADE_STAGE = 'database_stage';
21
22 const RESULT_OK = 'ok';
23 const RESULT_ERROR = 'error';
24
25 const STATUS_OK = 'ok';
26 const STATUS_NEED_INSTALL = 'need-install';
27 const STATUS_NEED_UPDATING = 'need-update';
28 const STATUS_FINISHED_INSTALL = 'finish-install';
29 const STATUS_FINISHED_UPDATING = 'finish-update';
30
31 /**
32 * Current upgrade stage
33 *
34 * @var string|false
35 */
36 private $stage = false;
37
38 /**
39 * List of all upgrade stages
40 *
41 * @var array<int, string>
42 */
43 private $stages = [];
44
45 /**
46 * Current database status
47 *
48 * @var string|false
49 */
50 private $status = false;
51
52 /**
53 * Result of last operation
54 *
55 * @var string|false
56 */
57 private $result = false;
58
59 /**
60 * Reason for current status
61 *
62 * @var string|false
63 */
64 private $reason = false;
65
66 /**
67 * Debug information
68 *
69 * @var array<int, string>
70 */
71 private $debug = [];
72
73 public function __construct() {
74 $this->status = self::STATUS_OK;
75
76 if ( $this->needs_installing() ) {
77 $this->status = self::STATUS_NEED_INSTALL;
78 }
79
80 $this->load_stage();
81
82 if ( $this->needs_updating() ) {
83 $this->status = self::STATUS_NEED_UPDATING;
84 }
85 }
86
87 /**
88 * Load current upgrade stage from options
89 *
90 * @return void
91 */
92 public function load_stage(): void {
93 $settings = Red_Options::get();
94
95 if ( isset( $settings[ self::DB_UPGRADE_STAGE ] ) ) {
96 $stage_data = $settings[ self::DB_UPGRADE_STAGE ];
97
98 // Database stage can be set to false to clear it - only process if it's an array
99 // phpcs:ignore function.alreadyNarrowedType
100 // @phpstan-ignore function.alreadyNarrowedType
101 if ( ! is_array( $stage_data ) ) {
102 return;
103 }
104
105 $this->stage = isset( $stage_data['stage'] ) ? $stage_data['stage'] : false;
106 $this->stages = isset( $stage_data['stages'] ) ? $stage_data['stages'] : [];
107
108 // Only override status if we have a saved upgrade in progress
109 if ( isset( $stage_data['status'] ) && $stage_data['status'] !== false ) {
110 $this->status = $stage_data['status'];
111 }
112 }
113 }
114
115 /**
116 * Does the database need install
117 *
118 * @return bool true if needs installing, false otherwise
119 */
120 public function needs_installing(): bool {
121 $settings = Red_Options::get();
122
123 if ( $settings['database'] === '' && $this->get_old_version() === false ) {
124 return true;
125 }
126
127 return false;
128 }
129
130 /**
131 * Does the current database need updating to the target
132 *
133 * @return bool true if needs updating, false otherwise
134 */
135 public function needs_updating(): bool {
136 // We need updating if we don't need to install, and the current version is less than target version
137 if ( $this->needs_installing() === false && version_compare( $this->get_current_version(), REDIRECTION_DB_VERSION, '<' ) ) {
138 return true;
139 }
140
141 // Also if we're still in the process of upgrading
142 if ( $this->get_current_stage() !== false && $this->status !== self::STATUS_NEED_INSTALL ) {
143 return true;
144 }
145
146 return false;
147 }
148
149 /**
150 * Get current database version
151 *
152 * @return string Current database version
153 */
154 public function get_current_version(): string {
155 $settings = Red_Options::get();
156
157 if ( $settings['database'] !== '' ) {
158 if ( $settings['database'] === '+OK' ) {
159 return REDIRECTION_DB_VERSION;
160 }
161
162 return $settings['database'];
163 }
164
165 $version = $this->get_old_version();
166 if ( $version !== false && $version !== '' && $version !== '0' && $version !== 0 ) {
167 // Upgrade the old value
168 $version = (string) $version;
169 red_set_options( array( 'database' => $version ) );
170 delete_option( self::OLD_DB_VERSION );
171 $this->clear_cache();
172 return $version;
173 }
174
175 return '';
176 }
177
178 /**
179 * Get old database version from legacy option
180 *
181 * @return mixed
182 */
183 private function get_old_version() {
184 return get_option( self::OLD_DB_VERSION );
185 }
186
187 /**
188 * Check if required database tables exist
189 *
190 * @return void
191 */
192 public function check_tables_exist(): void {
193 $latest = Red_Database::get_latest_database();
194 $missing = $latest->get_missing_tables();
195
196 // No tables installed - do a fresh install
197 if ( count( $missing ) === count( $latest->get_all_tables() ) ) {
198 delete_option( self::OLD_DB_VERSION );
199 red_set_options( [ 'database' => '' ] );
200 $this->clear_cache();
201
202 $this->status = self::STATUS_NEED_INSTALL;
203 $this->stop_update();
204 } elseif ( count( $missing ) > 0 && version_compare( $this->get_current_version(), '2.3.3', 'ge' ) ) {
205 // Some tables are missing - try and fill them in
206 $latest->install();
207 }
208 }
209
210 /**
211 * Does the current database support a particular version
212 *
213 * @param string $version Target version
214 * @return bool true if supported, false otherwise
215 */
216 public function does_support( string $version ): bool {
217 return version_compare( $this->get_current_version(), $version, 'ge' );
218 }
219
220 /**
221 * Check if last operation resulted in error
222 *
223 * @return bool
224 */
225 public function is_error(): bool {
226 return $this->result === self::RESULT_ERROR;
227 }
228
229 /**
230 * Set error status
231 *
232 * @param string $error Error message.
233 * @return void
234 */
235 public function set_error( string $error ): void {
236 global $wpdb;
237
238 $this->result = self::RESULT_ERROR;
239 $this->reason = str_replace( "\t", ' ', $error );
240
241 if ( $wpdb->last_error ) {
242 $this->debug[] = $wpdb->last_error;
243
244 if ( strpos( $wpdb->last_error, 'command denied to user' ) !== false ) {
245 $this->reason .= ' - ' . __( 'Insufficient database permissions detected. Please give your database user appropriate permissions.', 'redirection' );
246 }
247 }
248
249 $latest = Red_Database::get_latest_database();
250 $this->debug = array_merge( $this->debug, $latest->get_table_schema() );
251 $this->debug[] = 'Stage: ' . $this->get_current_stage();
252 }
253
254 /**
255 * Set success status
256 *
257 * @param string $reason Success message.
258 * @return void
259 */
260 public function set_ok( string $reason ): void {
261 $this->reason = $reason;
262 $this->result = self::RESULT_OK;
263 $this->debug = [];
264 }
265
266 /**
267 * Stop current upgrade
268 *
269 * @return void
270 */
271 public function stop_update(): void {
272 $this->stage = false;
273 $this->stages = [];
274 $this->debug = [];
275
276 red_set_options( [ self::DB_UPGRADE_STAGE => false ] );
277 $this->clear_cache();
278 }
279
280 /**
281 * Finish upgrade process
282 *
283 * @return void
284 */
285 public function finish(): void {
286 $this->stop_update();
287
288 if ( $this->status === self::STATUS_NEED_INSTALL ) {
289 $this->status = self::STATUS_FINISHED_INSTALL;
290 } elseif ( $this->status === self::STATUS_NEED_UPDATING ) {
291 $this->status = self::STATUS_FINISHED_UPDATING;
292 }
293 }
294
295 /**
296 * Get current upgrade stage
297 *
298 * @return string|false Current stage name, or false if not upgrading
299 */
300 public function get_current_stage() {
301 return $this->stage;
302 }
303
304 /**
305 * Move current stage on to the next
306 *
307 * @return void
308 */
309 public function set_next_stage(): void {
310 $this->debug = [];
311 $stage = $this->get_current_stage();
312
313 if ( $stage !== false ) {
314 $stage = $this->get_next_stage( $stage );
315
316 // Save next position
317 if ( $stage !== false ) {
318 $this->set_stage( $stage );
319 } else {
320 $this->finish();
321 }
322 }
323 }
324
325 /**
326 * Get current upgrade status
327 *
328 * @return DatabaseStatus Database status array
329 */
330 public function get_json() {
331 // Base information
332 $result = [
333 'status' => $this->status,
334 'inProgress' => $this->stage !== false,
335 ];
336
337 // Add on version status
338 if ( $this->status === self::STATUS_NEED_INSTALL || $this->status === self::STATUS_NEED_UPDATING ) {
339 $result = array_merge(
340 $result,
341 $this->get_version_upgrade(),
342 [ 'manual' => $this->get_manual_upgrade() ]
343 );
344 }
345
346 // Add on upgrade status
347 if ( $this->is_error() ) {
348 $result = array_merge( $result, $this->get_version_upgrade(), $this->get_progress_status(), $this->get_error_status() );
349 } elseif ( $result['inProgress'] ) {
350 $result = array_merge( $result, $this->get_progress_status() );
351 } elseif ( $this->status === self::STATUS_FINISHED_INSTALL || $this->status === self::STATUS_FINISHED_UPDATING ) {
352 $result['complete'] = 100;
353 $result['reason'] = $this->reason;
354 } elseif ( $this->status === self::STATUS_NEED_INSTALL || $this->status === self::STATUS_NEED_UPDATING ) {
355 // For fresh install/update that hasn't started yet, set initial progress state
356 $result['complete'] = 0;
357 $result['result'] = self::RESULT_OK;
358 $result['reason'] = false;
359 }
360
361 return $result;
362 }
363
364 /**
365 * Get error status information
366 *
367 * @phpstan-return array{reason: string|false, result: 'error', debug: array<int, string>}
368 * @return array<string, mixed>
369 */
370 private function get_error_status(): array {
371 return [
372 'reason' => $this->reason,
373 'result' => self::RESULT_ERROR,
374 'debug' => $this->debug,
375 ];
376 }
377
378 /**
379 * Get progress status information
380 *
381 * @return array{complete: int|float, result: 'ok', reason: string|false}
382 */
383 private function get_progress_status() {
384 $complete = 0;
385
386 if ( $this->stage !== false ) {
387 $total = count( $this->stages );
388 $pos = array_search( $this->stage, $this->stages, true );
389
390 if ( $pos !== false && $total > 0 ) {
391 $complete = round( ( $pos / $total ) * 100, 1 );
392 }
393 }
394
395 return [
396 'complete' => $complete,
397 'result' => self::RESULT_OK,
398 'reason' => $this->reason,
399 ];
400 }
401
402 /**
403 * Get version upgrade information
404 *
405 * @phpstan-return array{current: string, next: string, time: float}
406 * @return array<string, mixed>
407 */
408 private function get_version_upgrade(): array {
409 return [
410 'current' => $this->get_current_version() ? $this->get_current_version() : '-',
411 'next' => REDIRECTION_DB_VERSION,
412 'time' => microtime( true ),
413 ];
414 }
415
416 /**
417 * Set the status information for a database upgrade
418 *
419 * @param Red_Database_Upgrade[] $upgrades List of upgrade versions.
420 * @return void
421 */
422 public function start_install( array $upgrades ) {
423 $this->set_stages( $upgrades );
424 $this->status = self::STATUS_NEED_INSTALL;
425 }
426
427 /**
428 * Start database upgrade process
429 *
430 * @param Red_Database_Upgrade[] $upgrades List of upgrade versions.
431 * @return void
432 */
433 public function start_upgrade( array $upgrades ) {
434 $this->set_stages( $upgrades );
435 $this->status = self::STATUS_NEED_UPDATING;
436 }
437
438 /**
439 * Set upgrade stages
440 *
441 * @param Red_Database_Upgrade[] $upgrades List of upgrade versions.
442 * @return void
443 */
444 private function set_stages( array $upgrades ) {
445 $this->stages = [];
446
447 foreach ( $upgrades as $upgrade ) {
448 $upgrader = Red_Database_Upgrader::get( $upgrade );
449 $this->stages = array_merge( $this->stages, array_keys( $upgrader->get_stages() ) );
450 }
451
452 if ( count( $this->stages ) > 0 ) {
453 $this->set_stage( $this->stages[0] );
454 }
455 }
456
457 /**
458 * @param string|false $stage
459 * @return void
460 */
461 public function set_stage( $stage ): void {
462 $this->stage = $stage;
463 $this->save_details();
464 }
465
466 /**
467 * @return void
468 */
469 private function save_details(): void {
470 $stages = [
471 self::DB_UPGRADE_STAGE => [
472 'stage' => $this->stage,
473 'stages' => $this->stages,
474 'status' => $this->status,
475 ],
476 ];
477
478 red_set_options( $stages );
479
480 $this->clear_cache();
481 }
482
483 /**
484 * @return array<int, string>
485 */
486 private function get_manual_upgrade(): array {
487 $queries = [];
488 $database = new Red_Database();
489 $upgraders = $database->get_upgrades_for_version( $this->get_current_version(), false );
490
491 foreach ( $upgraders as $upgrade ) {
492 $upgrade = Red_Database_Upgrader::get( $upgrade );
493
494 $stages = $upgrade->get_stages();
495 foreach ( array_keys( $stages ) as $stage ) {
496 $queries = array_merge( $queries, $upgrade->get_queries_for_stage( $stage ) );
497 }
498 }
499
500 return $queries;
501 }
502
503 /**
504 * @param string $stage
505 * @return string|false
506 */
507 private function get_next_stage( string $stage ) {
508 $database = new Red_Database();
509 $upgraders = $database->get_upgrades_for_version( $this->get_current_version(), $this->get_current_stage() );
510
511 if ( count( $upgraders ) === 0 ) {
512 $upgraders = $database->get_upgrades_for_version( $this->get_current_version(), false );
513 }
514
515 if ( count( $upgraders ) === 0 ) {
516 return false;
517 }
518
519 $upgrader = Red_Database_Upgrader::get( $upgraders[0] );
520
521 // Where are we in this?
522 $pos = array_search( $stage, $this->stages, true );
523
524 if ( $pos === false ) {
525 return false;
526 }
527
528 if ( $pos === count( $this->stages ) - 1 ) {
529 $this->save_db_version( REDIRECTION_DB_VERSION );
530 return false;
531 }
532
533 // Set current DB version
534 $current_stages = array_keys( $upgrader->get_stages() );
535
536 $current_position = array_search( $stage, $current_stages, true );
537
538 if ( $current_position !== false && $current_position === count( $current_stages ) - 1 && isset( $upgraders[1] ) ) {
539 $this->save_db_version( $upgraders[1]->get_version() );
540 }
541
542 // Move on to next in current version
543 return $this->stages[ $pos + 1 ];
544 }
545
546 /**
547 * @param string $version
548 * @return void
549 */
550 public function save_db_version( string $version ): void {
551 red_set_options( array( 'database' => $version ) );
552 delete_option( self::OLD_DB_VERSION );
553
554 $this->clear_cache();
555 }
556
557 /**
558 * @return void
559 */
560 private function clear_cache(): void {
561 // Clear Red_Options in-memory cache
562 Red_Options::reset();
563
564 // Clear WordPress object cache if available
565 if ( file_exists( WP_CONTENT_DIR . '/object-cache.php' ) && function_exists( 'wp_cache_flush' ) ) {
566 wp_cache_flush();
567 }
568 }
569 }
570