PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.11.0
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.11.0
4.11.0 4.10.0 4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Framework / Database / TablesRenamer.php
wp-staging / Framework / Database Last commit date
Exporter 1 day ago QueryBuilder 1 day ago CustomTable.php 1 day ago DbInfo.php 1 day ago ExcludedTables.php 1 day ago ExternalDatabaseConfiguration.php 1 day ago OptionPreservationHandler.php 1 day ago SearchReplace.php 1 day ago SelectedTables.php 1 day ago TableDto.php 1 day ago TableService.php 1 day ago TablesRenamer.php 1 day ago WpDbInfo.php 1 day ago WpOptionsInfo.php 1 day ago iDbInfo.php 2 years ago
TablesRenamer.php
1241 lines
1 <?php
2
3 namespace WPStaging\Framework\Database;
4
5 use WPStaging\Backup\Dto\Task\Restore\RenameDatabaseTaskDto;
6 use WPStaging\Core\Utils\Logger;
7 use WPStaging\Framework\Adapter\PhpAdapter;
8 use WPStaging\Framework\Facades\Hooks;
9 use WPStaging\Framework\Traits\SerializeTrait;
10
11
12
13
14
15 class TablesRenamer
16 {
17 use SerializeTrait;
18
19
20 const OPTION_ACTIVE_PLUGINS = 'active_plugins';
21
22
23 const OPTION_ACTIVE_SITEWIDE_PLUGINS = 'active_sitewide_plugins';
24
25
26 const PLUGIN_BASE_SLUG = 'wp-staging';
27
28
29 private $tableService;
30
31
32
33
34
35
36
37
38
39 protected $tablesBeingRenamed = [];
40
41
42
43
44
45
46
47
48
49 protected $tablesBeingRenamedUnprefixed = [];
50
51
52
53
54
55
56
57
58
59 protected $existingTables = [];
60
61
62
63
64
65
66
67
68
69 protected $existingTablesUnprefixed = [];
70
71
72 protected $customTablesBeingRenamed = [];
73
74
75 protected $shortNamedTablesToRename = [];
76
77
78 protected $shortNamedTablesToDrop = [];
79
80
81 protected $excludedTables = [];
82
83
84 protected $tablesToBeDropped = [];
85
86
87 protected $tablesToPreserve = [];
88
89
90 protected $destinationSubsiteBlogIds = [];
91
92
93 protected $totalTables = 0;
94
95
96
97
98
99 protected $tablesRenamed = 0;
100
101
102 protected $tablesRemainingToBeDropped = 0;
103
104
105 protected $productionTablePrefix = '';
106
107
108 protected $productionTableBasePrefix = '';
109
110
111 protected $tmpPrefix = '';
112
113
114 protected $customTableTmpPrefix = '';
115
116
117 protected $dropPrefix = '';
118
119
120 protected $renameViews = false;
121
122
123 protected $renameCustomTables = false;
124
125
126 protected $logEachRename = false;
127
128
129 protected $logger = null;
130
131
132 protected $phpAdapter;
133
134
135 protected $thresholdCallable = null;
136
137
138 protected $conflictingTablesRenamed = 0;
139
140
141 protected $nonConflictingTablesRenamed = 0;
142
143
144 protected $customTablesRenamed = 0;
145
146
147 protected $isNonConflictingTablesRenamingTaskExecuted = false;
148
149
150 protected $isRenamingForSubsite = false;
151
152
153 protected $errors = [];
154
155 public function __construct(TableService $tableService, PhpAdapter $phpAdapter)
156 {
157 $this->tableService = $tableService;
158 $this->phpAdapter = $phpAdapter;
159 }
160
161
162
163
164
165 public function setProductionTablePrefix(string $productionTablePrefix): TablesRenamer
166 {
167 $this->productionTablePrefix = $productionTablePrefix;
168 return $this;
169 }
170
171
172
173
174
175
176 public function setProductionTableBasePrefix(string $productionTableBasePrefix, bool $isRenamingForSubsite = true): TablesRenamer
177 {
178 $this->productionTableBasePrefix = $productionTableBasePrefix;
179 $this->isRenamingForSubsite = $isRenamingForSubsite;
180 return $this;
181 }
182
183
184
185
186
187 public function setTmpPrefix(string $tmpPrefix): TablesRenamer
188 {
189 $this->tmpPrefix = $tmpPrefix;
190 return $this;
191 }
192
193
194
195
196
197 public function setCustomTableTmpPrefix(string $customTableTmpPrefix): TablesRenamer
198 {
199 $this->customTableTmpPrefix = $customTableTmpPrefix;
200 return $this;
201 }
202
203
204
205
206
207 public function setDropPrefix(string $dropPrefix): TablesRenamer
208 {
209 $this->dropPrefix = $dropPrefix;
210 return $this;
211 }
212
213
214
215
216
217 public function setRenameViews(bool $renameViews): TablesRenamer
218 {
219 $this->renameViews = $renameViews;
220 return $this;
221 }
222
223
224
225
226
227 public function setRenameCustomTables(bool $renameCustomTables): TablesRenamer
228 {
229 $this->renameCustomTables = $renameCustomTables;
230 return $this;
231 }
232
233
234
235
236
237 public function setLogEachRename(bool $logEachRename): TablesRenamer
238 {
239 $this->logEachRename = $logEachRename;
240 return $this;
241 }
242
243
244
245
246
247 public function setLogger(Logger $logger): TablesRenamer
248 {
249 $this->logger = $logger;
250 return $this;
251 }
252
253
254
255
256
257 public function setThresholdCallable($thresholdCallable): TablesRenamer
258 {
259 $this->thresholdCallable = $thresholdCallable;
260 return $this;
261 }
262
263
264
265
266
267 public function setShortNamedTablesToRename(array $shortNamedTablesToRename): TablesRenamer
268 {
269 $this->shortNamedTablesToRename = $shortNamedTablesToRename;
270 return $this;
271 }
272
273
274
275
276
277 public function setShortNamedTablesToDrop(array $shortNamedTablesToDrop): TablesRenamer
278 {
279 $this->shortNamedTablesToDrop = $shortNamedTablesToDrop;
280 return $this;
281 }
282
283
284
285
286
287 public function setExcludedTables(array $excludedTables): TablesRenamer
288 {
289 $this->excludedTables = $excludedTables;
290 return $this;
291 }
292
293
294
295
296
297 public function setTablesToPreserve(array $tablesToPreserve): TablesRenamer
298 {
299 $this->tablesToPreserve = $tablesToPreserve;
300 return $this;
301 }
302
303
304
305
306
307 public function setDestinationSubsiteBlogIds(array $blogIds): TablesRenamer
308 {
309 $this->destinationSubsiteBlogIds = [];
310 foreach ($blogIds as $blogId) {
311 $blogId = (int) $blogId;
312 if ($blogId > 1) {
313 $this->destinationSubsiteBlogIds[$blogId] = true;
314 }
315 }
316
317 return $this;
318 }
319
320
321 public function getRenamedTables(): int
322 {
323 return $this->tablesRenamed;
324 }
325
326
327 public function getTotalTables(): int
328 {
329 return $this->totalTables;
330 }
331
332
333 public function getTablesRemainingToBeDropped(): int
334 {
335 return $this->tablesRemainingToBeDropped;
336 }
337
338
339 public function getViewsToBeRenamed(): array
340 {
341 return $this->tablesBeingRenamedUnprefixed['views'];
342 }
343
344
345
346
347 public function getConflictingTablesRenamed(): int
348 {
349 return $this->conflictingTablesRenamed;
350 }
351
352
353
354
355 public function getNonConflictingTablesRenamed(): int
356 {
357 return $this->nonConflictingTablesRenamed;
358 }
359
360
361
362
363 public function getCustomTablesRenamed(): int
364 {
365 return $this->customTablesRenamed;
366 }
367
368
369
370
371 public function getIsNonConflictingTablesRenamingTaskExecuted(): bool
372 {
373 return $this->isNonConflictingTablesRenamingTaskExecuted;
374 }
375
376 public function getErrors(): array
377 {
378 return $this->errors;
379 }
380
381
382
383
384 public function resetErrors()
385 {
386 $this->errors = [];
387 }
388
389
390
391
392 public function setupRenamer(): RenameDatabaseTaskDto
393 {
394 $taskDto = new RenameDatabaseTaskDto();
395 $taskDto->tablesBeingRenamed = $this->tableService->findTableNamesStartWith($this->tmpPrefix) ?: [];
396 $taskDto->viewsBeingRenamed = [];
397 if ($this->renameViews) {
398 $taskDto->viewsBeingRenamed = $this->tableService->findViewsNamesStartWith($this->tmpPrefix) ?: [];
399 }
400
401 $taskDto->customTablesBeingRenamed = [];
402 if ($this->renameCustomTables) {
403 $taskDto->customTablesBeingRenamed = $this->tableService->findTableNamesStartWith($this->customTableTmpPrefix) ?: [];
404 }
405
406 $taskDto->existingTables = $this->tableService->findTableNamesStartWith($this->productionTablePrefix) ?: [];
407 if ($this->isRenamingForSubsite && !in_array($this->productionTableBasePrefix . 'users', $taskDto->existingTables)) {
408 $taskDto->existingTables[] = $this->productionTableBasePrefix . 'users';
409 $taskDto->existingTables[] = $this->productionTableBasePrefix . 'usermeta';
410 }
411
412 $taskDto->existingViews = [];
413 if ($this->renameViews) {
414 $taskDto->existingViews = $this->tableService->findViewsNamesStartWith($this->productionTablePrefix) ?: [];
415 }
416
417 $taskDto->conflictingTablesRenamed = 0;
418 $taskDto->nonConflictingTablesRenamed = 0;
419 $taskDto->customTablesRenamed = 0;
420 $this->setTaskDto($taskDto);
421
422 return $taskDto;
423 }
424
425
426
427
428
429 public function setTaskDto(RenameDatabaseTaskDto $taskDto)
430 {
431 $this->tablesBeingRenamed = [];
432 $this->tablesBeingRenamed['tables'] = $taskDto->tablesBeingRenamed ?: [];
433 $this->tablesBeingRenamed['views'] = $taskDto->viewsBeingRenamed ?: [];
434 $this->tablesBeingRenamed['custom'] = $taskDto->customTablesBeingRenamed ?: [];
435 $this->tablesBeingRenamed['all'] = array_merge($this->tablesBeingRenamed['tables'], $this->tablesBeingRenamed['views']);
436
437 $this->existingTables = [];
438 $this->existingTables['tables'] = $taskDto->existingTables ?: [];
439 $this->existingTables['views'] = $taskDto->existingViews ?: [];
440 $this->existingTables['all'] = array_merge($this->existingTables['tables'], $this->existingTables['views']);
441
442 $this->totalTables = count($this->tablesBeingRenamed['tables']) + count($this->tablesBeingRenamed['custom']);
443 $tmpDatabasePrefix = $this->tmpPrefix;
444
445 foreach ($this->tablesBeingRenamed as $viewsOrTables => $tableName) {
446 $this->tablesBeingRenamedUnprefixed[$viewsOrTables] = array_map(function ($tableName) use ($tmpDatabasePrefix) {
447 $tableName = $this->getFullNameTableFromShortName($tableName, $tmpDatabasePrefix);
448 return substr($tableName, strlen($tmpDatabasePrefix));
449 }, $this->tablesBeingRenamed[$viewsOrTables]);
450 }
451
452 $productionTablePrefix = $this->productionTablePrefix;
453 $isSubsiteRestore = $this->isRenamingForSubsite;
454 $baseProdTablePrefix = $this->productionTableBasePrefix;
455
456 foreach ($this->existingTables as $viewsOrTables => $tableName) {
457 $this->existingTablesUnprefixed[$viewsOrTables] = array_map(function ($tableName) use ($productionTablePrefix, $baseProdTablePrefix, $isSubsiteRestore) {
458 if ($isSubsiteRestore && in_array($tableName, [ $baseProdTablePrefix . 'users', $baseProdTablePrefix . 'usermeta' ])) {
459 return substr($tableName, strlen($baseProdTablePrefix));
460 }
461
462 return substr($tableName, strlen($productionTablePrefix));
463 }, $this->existingTables[$viewsOrTables]);
464 }
465
466 $this->conflictingTablesRenamed = (int)$taskDto->conflictingTablesRenamed;
467 $this->nonConflictingTablesRenamed = (int)$taskDto->nonConflictingTablesRenamed;
468 $this->customTablesRenamed = (int)$taskDto->customTablesRenamed;
469 }
470
471
472
473
474
475
476
477 public function getFullNameTableFromShortName(string $table, string $prefix): string
478 {
479 $shortTables = [];
480 if ($prefix === $this->tmpPrefix) {
481 $shortTables = $this->shortNamedTablesToRename;
482 } elseif ($prefix === $this->dropPrefix) {
483 $shortTables = $this->shortNamedTablesToDrop;
484 }
485
486 if (!array_key_exists($table, $shortTables)) {
487 return $table;
488 }
489
490 return $shortTables[$table];
491 }
492
493
494
495
496
497
498 public function getTableShortName(string $table, string $prefix)
499 {
500 $shortTables = [];
501 if ($prefix === $this->tmpPrefix) {
502 $shortTables = $this->shortNamedTablesToRename;
503 } elseif ($prefix === $this->dropPrefix) {
504 $shortTables = $this->shortNamedTablesToDrop;
505 }
506
507 return array_search($table, $shortTables);
508 }
509
510
511
512
513
514 public function renameConflictingTables(): bool
515 {
516 $conflictingTablesWithoutPrefix = array_values($this->getTablesThatExistInBothExistingAndTempUnprefixed());
517
518 if (empty($conflictingTablesWithoutPrefix)) {
519 return true;
520 }
521
522
523 if (count($conflictingTablesWithoutPrefix) <= $this->conflictingTablesRenamed) {
524 return true;
525 }
526
527 $this->tableService->getDatabase()->exec('START TRANSACTION;');
528 for ($i = $this->conflictingTablesRenamed; $i < count($conflictingTablesWithoutPrefix); $i++) {
529 $conflictingTableWithoutPrefix = $conflictingTablesWithoutPrefix[$i];
530 if ($this->isExcludedTable($conflictingTableWithoutPrefix)) {
531 $this->tablesRenamed++;
532 $this->conflictingTablesRenamed++;
533 continue;
534 }
535
536 if ($this->isTableToPreserve($conflictingTableWithoutPrefix)) {
537 $this->tablesRenamed++;
538 $this->nonConflictingTablesRenamed++;
539 continue;
540 }
541
542 $currentTable = $this->getCurrentSiteTable($conflictingTableWithoutPrefix);
543 $tableToDrop = $this->getTableShortName($currentTable, $this->dropPrefix);
544 if ($tableToDrop === false) {
545 $tableToDrop = $this->dropPrefix . $conflictingTableWithoutPrefix;
546 }
547
548
549 $result = $this->tableService->getDatabase()->exec(sprintf(
550 "RENAME TABLE `%s` TO `%s`;",
551 $currentTable,
552 $tableToDrop
553 ));
554
555 if ($result === false && ($this->logEachRename && $this->logger instanceof Logger)) {
556
557 $wpdb = $this->tableService->getDatabase()->getWpdba()->getClient();
558 $error = $wpdb->last_error;
559 $this->logger->warning("DB Rename: Unable to rename table {$currentTable} to {$tableToDrop}. Error: " . $error);
560 }
561
562 $this->renameTable($conflictingTableWithoutPrefix, $this->conflictingTablesRenamed);
563
564 if ($this->isThresholdReached()) {
565 $this->tableService->getDatabase()->exec('COMMIT;');
566 return false;
567 }
568 }
569
570 $this->tableService->getDatabase()->exec('COMMIT;');
571
572 return true;
573 }
574
575
576
577
578
579 public function renameNonConflictingTables(): bool
580 {
581 $nonConflictingTables = array_values($this->getTablesThatExistInTempButNotInSite());
582
583 if (empty($nonConflictingTables)) {
584 return true;
585 }
586
587
588 if (count($nonConflictingTables) <= $this->nonConflictingTablesRenamed) {
589 return true;
590 }
591
592 $this->tableService->getDatabase()->exec('START TRANSACTION;');
593 for ($i = $this->nonConflictingTablesRenamed; $i < count($nonConflictingTables); $i++) {
594 $nonConflictingTable = $nonConflictingTables[$i];
595 if ($this->isExcludedTable($nonConflictingTable)) {
596 $this->tablesRenamed++;
597 $this->nonConflictingTablesRenamed++;
598 continue;
599 }
600
601 if ($this->isTableToPreserve($nonConflictingTable)) {
602 $this->tablesRenamed++;
603 $this->nonConflictingTablesRenamed++;
604 continue;
605 }
606
607 $this->renameTable($nonConflictingTable, $this->nonConflictingTablesRenamed);
608 $this->isNonConflictingTablesRenamingTaskExecuted = true;
609
610 if ($this->isThresholdReached()) {
611 $this->tableService->getDatabase()->exec('COMMIT;');
612 return false;
613 }
614 }
615
616 $this->tableService->getDatabase()->exec('COMMIT;');
617
618 return true;
619 }
620
621
622
623
624 public function cleanTemporaryBackupTables(): bool
625 {
626
627 if ($this->nonConflictingTablesRenamed !== 0 || $this->conflictingTablesRenamed !== 0) {
628 return true;
629 }
630
631 $this->tablesToBeDropped = $this->tableService->findTableNamesStartWith($this->dropPrefix) ?: [];
632 $this->tablesRemainingToBeDropped = count($this->tablesToBeDropped);
633
634 $this->tableService->getDatabase()->exec('SET autocommit=0;');
635 $this->tableService->getDatabase()->exec('SET FOREIGN_KEY_CHECKS=0;');
636 $this->tableService->getDatabase()->exec('START TRANSACTION;');
637 foreach ($this->tablesToBeDropped as $table) {
638 $result = $this->tableService->getDatabase()->exec(sprintf(
639 "DROP TABLE `%s`;",
640 $table
641 ));
642
643
644 if ($result === false) {
645 $this->tableService->getDatabase()->exec('COMMIT;');
646 $this->tableService->getDatabase()->exec('SET autocommit=1;');
647 return false;
648 }
649
650 $this->tablesRemainingToBeDropped--;
651 }
652
653 $this->tableService->getDatabase()->exec('COMMIT;');
654 $this->tableService->getDatabase()->exec('SET autocommit=1;');
655 return true;
656 }
657
658
659
660
661 public function renameTablesToDrop()
662 {
663 foreach ($this->getTablesThatExistInSiteButNotInTemp() as $table) {
664 if ($this->isTableToPreserve($table) || $this->isExistingDestinationSubsiteTable($table)) {
665 continue;
666 }
667
668 $fullTableName = $this->productionTablePrefix . $table;
669 $tableToDrop = $this->getTableShortName($fullTableName, $this->dropPrefix);
670 if ($tableToDrop === false) {
671 $tableToDrop = $this->dropPrefix . $table;
672 }
673
674 $this->tableService->getDatabase()->exec(sprintf(
675 "RENAME TABLE `%s` TO `%s`;",
676 $fullTableName,
677 $tableToDrop
678 ));
679 }
680 }
681
682
683
684
685
686 public function renameCustomTables(): bool
687 {
688 $customTablesToRename = $this->tablesBeingRenamed['custom'];
689
690 if (empty($customTablesToRename)) {
691 return true;
692 }
693
694 foreach ($customTablesToRename as $tmpCustomTable) {
695 $customTable = substr($tmpCustomTable, strlen($this->customTableTmpPrefix));
696
697 $result = true;
698 if ($this->tableExists($customTable)) {
699 $result = $this->renameQuery($customTable, $this->dropPrefix . $customTable);
700 }
701
702 if ($result === false) {
703 throw new \RuntimeException("Unable to rename custom table {$customTable} to {$this->dropPrefix}{$customTable}.");
704 }
705
706 $result = $this->renameQuery($tmpCustomTable, $customTable);
707 if ($result === false) {
708 throw new \RuntimeException("Unable to rename custom table {$tmpCustomTable} to {$customTable}.");
709 }
710
711 $this->customTablesRenamed++;
712 if ($this->isThresholdReached()) {
713 return false;
714 }
715 }
716
717 return true;
718 }
719
720
721
722
723
724
725 public function getActivePluginsToPreserve(): string
726 {
727 $tmpOptionsTable = $this->tmpPrefix . 'options';
728 if (!$this->tableExists($tmpOptionsTable)) {
729 return '';
730 }
731
732 $productionOptionsTable = $this->productionTablePrefix . 'options';
733 $activePluginsToPreserve = $this->getOptionValue($tmpOptionsTable, self::OPTION_ACTIVE_PLUGINS);
734 $currentActivePlugins = $this->getOptionValue($productionOptionsTable, self::OPTION_ACTIVE_PLUGINS);
735
736 if (empty($activePluginsToPreserve)) {
737 return $activePluginsToPreserve;
738 }
739
740
741 $wpstgActivePlugins = $this->safeMaybeUnserialize($currentActivePlugins);
742
743 if (!is_array($wpstgActivePlugins)) {
744
745 $this->insertOrUpdateOptionValue($productionOptionsTable, self::OPTION_ACTIVE_PLUGINS . '_bak', $currentActivePlugins);
746 $this->errors[] = 'The active plugins option in the database is corrupted in the current site. WP Staging has disabled all plugins during the restore process to avoid fatal errors. Nothing to worry about, the active plugins list is going to be replaced. However, the original value has been backed up in the options table with the name "active_plugins_bak".';
747 $wpstgActivePlugins = [];
748 }
749
750 $wpstgActivePlugins = array_filter($wpstgActivePlugins, function ($pluginSlug) {
751 return is_string($pluginSlug) && strpos($pluginSlug, self::PLUGIN_BASE_SLUG) === 0;
752 });
753
754 $wpstgActivePlugins = serialize($wpstgActivePlugins);
755 $this->updateOptionValue($tmpOptionsTable, self::OPTION_ACTIVE_PLUGINS, $wpstgActivePlugins);
756 $this->updateOptionValue($productionOptionsTable, self::OPTION_ACTIVE_PLUGINS, $wpstgActivePlugins);
757
758 return $activePluginsToPreserve;
759 }
760
761
762
763
764
765
766 public function getActiveSitewidePluginsToPreserve(): string
767 {
768 $tmpSiteMetaTable = $this->tmpPrefix . 'sitemeta';
769 if (!$this->tableExists($tmpSiteMetaTable)) {
770 return '';
771 }
772
773 $productionSiteMetaTable = $this->productionTablePrefix . 'sitemeta';
774 $activePluginsToPreserve = $this->getNetworkOptionValue($tmpSiteMetaTable, self::OPTION_ACTIVE_SITEWIDE_PLUGINS);
775 $currentActivePlugins = $this->getNetworkOptionValue($productionSiteMetaTable, self::OPTION_ACTIVE_SITEWIDE_PLUGINS);
776
777
778 $wpstgActivePlugins = $this->safeMaybeUnserialize($currentActivePlugins);
779
780 if (!is_array($wpstgActivePlugins)) {
781
782 $this->insertOrUpdateNetworkOptionValue($productionSiteMetaTable, self::OPTION_ACTIVE_SITEWIDE_PLUGINS . '_bak', $currentActivePlugins);
783 $this->errors[] = 'The active sitewide plugins option in the database is corrupted in the current site. WP Staging has disabled all sitewide plugins during the restore process to avoid fatal errors. Nothing to worry about, the active sitewide plugins option is going to be replaced anyway after the restore. However, the original value has been backed up in the sitemeta table with the name "active_sitewide_plugins_bak".';
784 $wpstgActivePlugins = [];
785 }
786
787 $wpstgActivePlugins = array_filter($wpstgActivePlugins, function ($pluginSlug) {
788 return is_string($pluginSlug) && strpos($pluginSlug, self::PLUGIN_BASE_SLUG) === 0;
789 }, ARRAY_FILTER_USE_KEY);
790
791 $wpstgActivePlugins = serialize($wpstgActivePlugins);
792 $this->updateNetworkOptionValue($tmpSiteMetaTable, self::OPTION_ACTIVE_SITEWIDE_PLUGINS, $wpstgActivePlugins);
793 $this->updateNetworkOptionValue($productionSiteMetaTable, self::OPTION_ACTIVE_SITEWIDE_PLUGINS, $wpstgActivePlugins);
794
795 return $activePluginsToPreserve;
796 }
797
798
799
800
801
802
803
804 public function restorePreservedActivePlugins(string $activePlugins, string $activeWpstgPlugin, bool $isNetworkActivatedPlugin): bool
805 {
806 $productionOptionsTable = $this->productionTablePrefix . 'options';
807 $preservedPlugins = $this->safeMaybeUnserialize($activePlugins);
808
809 if ($isNetworkActivatedPlugin) {
810 if (!is_array($preservedPlugins)) {
811 $this->addRejectedActivePluginsError($activePlugins);
812 $preservedPlugins = [];
813 }
814
815 $preservedPlugins = array_values(array_filter($preservedPlugins, 'is_string'));
816
817 return $this->updateOptionValue($productionOptionsTable, self::OPTION_ACTIVE_PLUGINS, serialize($preservedPlugins));
818 }
819
820
821 if (!is_array($preservedPlugins)) {
822
823 $this->insertOrUpdateOptionValue($productionOptionsTable, self::OPTION_ACTIVE_PLUGINS . '_bak', $activePlugins);
824 $this->errors[] = 'The active plugins option in the database is corrupted after the renamed table. WP Staging has disabled all plugins during the restore process to avoid fatal errors. You can re-activate your plugins from the WordPress admin dashboard after the restore is complete. The original value has been backed up in the options table with the name "active_plugins_bak".';
825 $preservedPlugins = [];
826 }
827
828 $preservedPlugins = array_filter($preservedPlugins, function ($pluginSlug) {
829 if (!is_string($pluginSlug)) {
830 return false;
831 }
832
833
834 if (strpos($pluginSlug, self::PLUGIN_BASE_SLUG) !== false) {
835 return false;
836 }
837
838 return true;
839 });
840
841
842 $preservedPlugins[] = $activeWpstgPlugin;
843 sort($preservedPlugins);
844
845 return $this->updateOptionValue($productionOptionsTable, self::OPTION_ACTIVE_PLUGINS, serialize($preservedPlugins));
846 }
847
848
849
850
851
852 protected function addRejectedActivePluginsError(string $originalValue)
853 {
854 if ($originalValue === '') {
855 return;
856 }
857
858 $this->errors[] = 'The active plugins option in the backup is corrupted or contains a serialized PHP object. WP Staging has disabled all plugins during the restore process to avoid fatal errors. You can re-activate your plugins from the WordPress admin dashboard after the restore is complete.';
859 }
860
861
862
863
864
865
866
867 public function restorePreservedActiveSitewidePlugins(string $activeSitewidePlugins, string $activeWpstgPlugin, $time = null): bool
868 {
869 $preservedSitewidePlugins = $this->safeMaybeUnserialize($activeSitewidePlugins);
870
871 if (!is_array($preservedSitewidePlugins)) {
872
873 $this->insertOrUpdateNetworkOptionValue($this->productionTablePrefix . 'sitemeta', self::OPTION_ACTIVE_SITEWIDE_PLUGINS . '_bak', $activeSitewidePlugins);
874 $this->errors[] = 'The active sitewide plugins option in the database is corrupted after the renamed table. WP Staging has disabled all sitewide plugins during the restore process to avoid fatal errors. You can re-activate your sitewide plugins from the WordPress admin dashboard after the restore is complete. The original value has been backed up in the sitemeta table with the name "active_sitewide_plugins_bak".';
875 $preservedSitewidePlugins = [];
876 }
877
878 $activeSitewidePlugins = array_filter($preservedSitewidePlugins, function ($pluginSlug) {
879
880
881 if (strpos($pluginSlug, self::PLUGIN_BASE_SLUG) !== false) {
882 return false;
883 }
884
885 return true;
886 });
887
888 if (!empty($activeWpstgPlugin)) {
889 $activeSitewidePlugins[$activeWpstgPlugin] = empty($time) ? time() : $time;
890 }
891
892 return $this->updateNetworkOptionValue($this->productionTablePrefix . 'sitemeta', self::OPTION_ACTIVE_SITEWIDE_PLUGINS, serialize($activeSitewidePlugins));
893 }
894
895 public function preserveTmpOption(string $optionName): bool
896 {
897 $tmpOptionsTable = $this->tmpPrefix . 'options';
898 if (!$this->tableExists($tmpOptionsTable)) {
899 return false;
900 }
901
902 $optionsTable = $this->productionTablePrefix . 'options';
903 $optionValue = $this->getOptionValue($optionsTable, $optionName);
904 if (empty($optionValue)) {
905 return false;
906 }
907
908 if ($this->getOptionValue($tmpOptionsTable, $optionName)) {
909 return $this->updateOptionValue($tmpOptionsTable, $optionName, $optionValue);
910 }
911
912 return $this->insertOptionValue($tmpOptionsTable, $optionName, $optionValue);
913 }
914
915
916
917
918
919 protected function isExcludedTable(string $tableName): bool
920 {
921 return in_array($tableName, $this->excludedTables);
922 }
923
924
925
926
927
928 protected function isTableToPreserve(string $tableName): bool
929 {
930 return in_array($tableName, $this->tablesToPreserve, true);
931 }
932
933
934
935
936
937 protected function isExistingDestinationSubsiteTable(string $tableName): bool
938 {
939 if (!$this->isRenamingForSubsite || strcasecmp($this->productionTablePrefix, $this->productionTableBasePrefix) !== 0) {
940 return false;
941 }
942
943 if (preg_match('/^(\d+)_/', $tableName, $matches) !== 1) {
944 return false;
945 }
946
947 $blogId = (int) $matches[1];
948 return (string) $blogId === $matches[1] && isset($this->destinationSubsiteBlogIds[$blogId]);
949 }
950
951
952
953
954 protected function getTablesThatExistInBothExistingAndTempUnprefixed(): array
955 {
956 return array_intersect($this->tablesBeingRenamedUnprefixed['all'], $this->existingTablesUnprefixed['all']);
957 }
958
959
960
961
962 protected function getTablesThatExistInSiteButNotInTemp(): array
963 {
964 return array_diff($this->existingTablesUnprefixed['all'], $this->tablesBeingRenamedUnprefixed['all']);
965 }
966
967
968
969
970 protected function getTablesThatExistInTempButNotInSite(): array
971 {
972 return array_diff($this->tablesBeingRenamedUnprefixed['all'], $this->existingTablesUnprefixed['all']);
973 }
974
975
976
977
978
979
980 protected function renameTable(string $tableWithoutPrefix, int &$tablesRenamed)
981 {
982 $tmpDatabasePrefix = $this->tmpPrefix;
983 $tableToRename = $tmpDatabasePrefix . $tableWithoutPrefix;
984 $tmpName = $this->getTableShortName($tableToRename, $tmpDatabasePrefix);
985 $tableAfterRenamed = $this->getCurrentSiteTable($tableWithoutPrefix);
986
987 if ($tmpName !== false) {
988 $tableToRename = $tmpName;
989 }
990
991
992 $database = $this->tableService->getDatabase();
993 $result = $database->exec(sprintf(
994 "RENAME TABLE `%s` TO `%s`;",
995 $tableToRename,
996 $tableAfterRenamed
997 ));
998
999 if ($result !== false) {
1000 $this->tablesRenamed++;
1001 $tablesRenamed++;
1002 if ($this->logEachRename && $this->logger instanceof Logger) {
1003 $this->logger->info("DB Rename: Renamed table {$tableToRename} to {$tableAfterRenamed}.");
1004 }
1005
1006 return;
1007 }
1008
1009 if ($this->logEachRename && $this->logger instanceof Logger) {
1010
1011 $wpdb = $database->getWpdba()->getClient();
1012 $error = $wpdb->last_error;
1013 $this->logger->warning("DB Rename: Unable to rename table {$tableToRename} to {$tableAfterRenamed}. Error: " . $error);
1014 }
1015 }
1016
1017
1018
1019
1020
1021 protected function tableExists(string $tableName): bool
1022 {
1023 $database = $this->tableService->getDatabase()->getWpdba()->getClient();
1024 $result = $database->get_results(
1025 $database->prepare("SHOW TABLES LIKE %s", $database->esc_like($tableName)),
1026 ARRAY_A
1027 );
1028
1029 return !empty($result);
1030 }
1031
1032
1033
1034
1035
1036
1037 protected function getOptionValue(string $tableName, string $optionName): string
1038 {
1039 $database = $this->tableService->getDatabase()->getWpdba()->getClient();
1040 $result = $database->get_results(
1041 $database->prepare(
1042 "SELECT option_value FROM `{$tableName}` WHERE option_name LIKE %s",
1043 $database->esc_like($optionName)
1044 ),
1045 ARRAY_A
1046 );
1047 if (empty($result)) {
1048 return '';
1049 }
1050
1051 return $result[0]['option_value'];
1052 }
1053
1054
1055
1056
1057
1058
1059
1060 protected function updateOptionValue(string $tableName, string $optionName, string $optionValue): bool
1061 {
1062 $database = $this->tableService->getDatabase()->getWpdba()->getClient();
1063
1064 return $database->query(
1065 $database->prepare(
1066 "UPDATE `{$tableName}` SET option_value = %s WHERE option_name LIKE %s",
1067 $optionValue,
1068 $database->esc_like($optionName)
1069 )
1070 );
1071 }
1072
1073
1074
1075
1076
1077
1078
1079
1080 protected function insertOptionValue(string $tableName, string $optionName, string $optionValue, bool $autoload = false): bool
1081 {
1082 $database = $this->tableService->getDatabase()->getWpdba()->getClient();
1083
1084 return $database->query(
1085 $database->prepare(
1086 "INSERT INTO `{$tableName}` (option_name, option_value, autoload) VALUES (%s, %s, %s)",
1087 $optionName,
1088 $optionValue,
1089 $autoload ? 'yes' : 'no'
1090 )
1091 );
1092 }
1093
1094
1095
1096
1097
1098
1099
1100
1101 protected function insertOrUpdateOptionValue(string $tableName, string $optionName, string $optionValue, bool $autoload = false): bool
1102 {
1103 if ($this->getOptionValue($tableName, $optionName)) {
1104 return $this->updateOptionValue($tableName, $optionName, $optionValue);
1105 }
1106
1107 return $this->insertOptionValue($tableName, $optionName, $optionValue, $autoload);
1108 }
1109
1110
1111
1112
1113
1114
1115 protected function getNetworkOptionValue(string $tableName, string $optionName): string
1116 {
1117 $database = $this->tableService->getDatabase()->getWpdba()->getClient();
1118 $result = $database->get_results(
1119 $database->prepare(
1120 "SELECT meta_value FROM `{$tableName}` WHERE meta_key LIKE %s",
1121 $database->esc_like($optionName)
1122 ),
1123 ARRAY_A
1124 );
1125 if (empty($result)) {
1126 return '';
1127 }
1128
1129 return $result[0]['meta_value'];
1130 }
1131
1132
1133
1134
1135
1136
1137
1138 protected function insertNetworkOptionValue(string $tableName, string $optionName, string $optionValue): bool
1139 {
1140 $database = $this->tableService->getDatabase()->getWpdba()->getClient();
1141
1142 return $database->query(
1143 $database->prepare(
1144 "INSERT INTO `{$tableName}` (meta_key, meta_value) VALUES (%s, %s)",
1145 $optionName,
1146 $optionValue
1147 )
1148 );
1149 }
1150
1151
1152
1153
1154
1155
1156
1157 protected function updateNetworkOptionValue(string $tableName, string $optionName, string $optionValue): bool
1158 {
1159 $database = $this->tableService->getDatabase()->getWpdba()->getClient();
1160
1161 return $database->query(
1162 $database->prepare(
1163 "UPDATE `{$tableName}` SET meta_value = %s WHERE meta_key LIKE %s",
1164 $optionValue,
1165 $database->esc_like($optionName)
1166 )
1167 );
1168 }
1169
1170
1171
1172
1173
1174
1175
1176 protected function insertOrUpdateNetworkOptionValue(string $tableName, string $optionName, string $optionValue): bool
1177 {
1178 if ($this->getNetworkOptionValue($tableName, $optionName)) {
1179 return $this->updateNetworkOptionValue($tableName, $optionName, $optionValue);
1180 }
1181
1182 return $this->insertNetworkOptionValue($tableName, $optionName, $optionValue);
1183 }
1184
1185
1186
1187
1188 protected function isThresholdReached(): bool
1189 {
1190 if (!$this->phpAdapter->isCallable($this->thresholdCallable)) {
1191 return $this->customThreshold(false);
1192 }
1193
1194 $result = call_user_func($this->thresholdCallable);
1195 return $this->customThreshold($result);
1196 }
1197
1198
1199
1200
1201
1202 private function customThreshold(bool $isThreshold): bool
1203 {
1204 return Hooks::applyFilters('wpstg.tests.tablesRenamingThreshold', $isThreshold);
1205 }
1206
1207
1208
1209
1210
1211
1212 private function renameQuery(string $tableToRename, string $tableAfterRenamed): bool
1213 {
1214 $result = $this->tableService->renameTable($tableToRename, $tableAfterRenamed);
1215 if ($result !== false) {
1216 return true;
1217 }
1218
1219 if ($this->logEachRename && $this->logger instanceof Logger) {
1220
1221 $error = $this->tableService->getLastWpdbError();
1222 $this->logger->warning(sprintf("DB Rename: Unable to rename table %s to %s. Error: %s", $tableToRename, $tableAfterRenamed, $error));
1223 }
1224
1225 return false;
1226 }
1227
1228 private function getCurrentSiteTable(string $tableWithoutPrefix): string
1229 {
1230 if ($tableWithoutPrefix !== 'users' && $tableWithoutPrefix !== 'usermeta') {
1231 return $this->productionTablePrefix . $tableWithoutPrefix;
1232 }
1233
1234 if (!$this->isRenamingForSubsite) {
1235 return $this->productionTablePrefix . $tableWithoutPrefix;
1236 }
1237
1238 return $this->productionTableBasePrefix . $tableWithoutPrefix;
1239 }
1240 }
1241