PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.11.1
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.11.1
4.11.2 4.11.1 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 week ago QueryBuilder 1 week ago CustomTable.php 1 week ago DbInfo.php 1 week ago ExcludedTables.php 1 week ago ExternalDatabaseConfiguration.php 1 week ago OptionPreservationHandler.php 1 week ago SearchReplace.php 1 week ago SelectedTables.php 1 week ago TableDto.php 1 week ago TableService.php 1 week ago TablesRenamer.php 1 week ago WpDbInfo.php 1 week ago WpOptionsInfo.php 1 week ago iDbInfo.php 3 years ago
TablesRenamer.php
1317 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 $stopOnRenameFailure = false;
130
131
132 protected $logger = null;
133
134
135 protected $phpAdapter;
136
137
138 protected $thresholdCallable = null;
139
140
141 protected $conflictingTablesRenamed = 0;
142
143
144 protected $nonConflictingTablesRenamed = 0;
145
146
147 protected $customTablesRenamed = 0;
148
149
150 protected $isNonConflictingTablesRenamingTaskExecuted = false;
151
152
153 protected $isRenamingForSubsite = false;
154
155
156 protected $errors = [];
157
158 public function __construct(TableService $tableService, PhpAdapter $phpAdapter)
159 {
160 $this->tableService = $tableService;
161 $this->phpAdapter = $phpAdapter;
162 }
163
164
165
166
167
168 public function setProductionTablePrefix(string $productionTablePrefix): TablesRenamer
169 {
170 $this->productionTablePrefix = $productionTablePrefix;
171 return $this;
172 }
173
174
175
176
177
178
179 public function setProductionTableBasePrefix(string $productionTableBasePrefix, bool $isRenamingForSubsite = true): TablesRenamer
180 {
181 $this->productionTableBasePrefix = $productionTableBasePrefix;
182 $this->isRenamingForSubsite = $isRenamingForSubsite;
183 return $this;
184 }
185
186
187
188
189
190 public function setTmpPrefix(string $tmpPrefix): TablesRenamer
191 {
192 $this->tmpPrefix = $tmpPrefix;
193 return $this;
194 }
195
196
197
198
199
200 public function setCustomTableTmpPrefix(string $customTableTmpPrefix): TablesRenamer
201 {
202 $this->customTableTmpPrefix = $customTableTmpPrefix;
203 return $this;
204 }
205
206
207
208
209
210 public function setDropPrefix(string $dropPrefix): TablesRenamer
211 {
212 $this->dropPrefix = $dropPrefix;
213 return $this;
214 }
215
216
217
218
219
220 public function setRenameViews(bool $renameViews): TablesRenamer
221 {
222 $this->renameViews = $renameViews;
223 return $this;
224 }
225
226
227
228
229
230 public function setRenameCustomTables(bool $renameCustomTables): TablesRenamer
231 {
232 $this->renameCustomTables = $renameCustomTables;
233 return $this;
234 }
235
236
237
238
239
240 public function setLogEachRename(bool $logEachRename): TablesRenamer
241 {
242 $this->logEachRename = $logEachRename;
243 return $this;
244 }
245
246
247
248
249
250 public function setStopOnRenameFailure(bool $stopOnRenameFailure): TablesRenamer
251 {
252 $this->stopOnRenameFailure = $stopOnRenameFailure;
253 return $this;
254 }
255
256
257
258
259
260 public function setLogger(Logger $logger): TablesRenamer
261 {
262 $this->logger = $logger;
263 return $this;
264 }
265
266
267
268
269
270 public function setThresholdCallable($thresholdCallable): TablesRenamer
271 {
272 $this->thresholdCallable = $thresholdCallable;
273 return $this;
274 }
275
276
277
278
279
280 public function setShortNamedTablesToRename(array $shortNamedTablesToRename): TablesRenamer
281 {
282 $this->shortNamedTablesToRename = $shortNamedTablesToRename;
283 return $this;
284 }
285
286
287
288
289
290 public function setShortNamedTablesToDrop(array $shortNamedTablesToDrop): TablesRenamer
291 {
292 $this->shortNamedTablesToDrop = $shortNamedTablesToDrop;
293 return $this;
294 }
295
296
297
298
299
300 public function setExcludedTables(array $excludedTables): TablesRenamer
301 {
302 $this->excludedTables = $excludedTables;
303 return $this;
304 }
305
306
307
308
309
310 public function setTablesToPreserve(array $tablesToPreserve): TablesRenamer
311 {
312 $this->tablesToPreserve = $tablesToPreserve;
313 return $this;
314 }
315
316
317
318
319
320 public function setDestinationSubsiteBlogIds(array $blogIds): TablesRenamer
321 {
322 $this->destinationSubsiteBlogIds = [];
323 foreach ($blogIds as $blogId) {
324 $blogId = (int) $blogId;
325 if ($blogId > 1) {
326 $this->destinationSubsiteBlogIds[$blogId] = true;
327 }
328 }
329
330 return $this;
331 }
332
333
334 public function getRenamedTables(): int
335 {
336 return $this->tablesRenamed;
337 }
338
339
340 public function getTotalTables(): int
341 {
342 return $this->totalTables;
343 }
344
345
346 public function getTablesRemainingToBeDropped(): int
347 {
348 return $this->tablesRemainingToBeDropped;
349 }
350
351
352 public function getViewsToBeRenamed(): array
353 {
354 return $this->tablesBeingRenamedUnprefixed['views'];
355 }
356
357
358
359
360 public function getConflictingTablesRenamed(): int
361 {
362 return $this->conflictingTablesRenamed;
363 }
364
365
366
367
368 public function getNonConflictingTablesRenamed(): int
369 {
370 return $this->nonConflictingTablesRenamed;
371 }
372
373
374
375
376 public function getCustomTablesRenamed(): int
377 {
378 return $this->customTablesRenamed;
379 }
380
381
382
383
384 public function getIsNonConflictingTablesRenamingTaskExecuted(): bool
385 {
386 return $this->isNonConflictingTablesRenamingTaskExecuted;
387 }
388
389 public function getErrors(): array
390 {
391 return $this->errors;
392 }
393
394
395
396
397 public function resetErrors()
398 {
399 $this->errors = [];
400 }
401
402
403
404
405 public function setupRenamer(): RenameDatabaseTaskDto
406 {
407 $taskDto = new RenameDatabaseTaskDto();
408 $taskDto->tablesBeingRenamed = $this->tableService->findTableNamesStartWith($this->tmpPrefix) ?: [];
409 $taskDto->viewsBeingRenamed = [];
410 if ($this->renameViews) {
411 $taskDto->viewsBeingRenamed = $this->tableService->findViewsNamesStartWith($this->tmpPrefix) ?: [];
412 }
413
414 $taskDto->customTablesBeingRenamed = [];
415 if ($this->renameCustomTables) {
416 $taskDto->customTablesBeingRenamed = $this->tableService->findTableNamesStartWith($this->customTableTmpPrefix) ?: [];
417 }
418
419 $taskDto->existingTables = $this->tableService->findTableNamesStartWith($this->productionTablePrefix) ?: [];
420 if ($this->isRenamingForSubsite && !in_array($this->productionTableBasePrefix . 'users', $taskDto->existingTables)) {
421 $taskDto->existingTables[] = $this->productionTableBasePrefix . 'users';
422 $taskDto->existingTables[] = $this->productionTableBasePrefix . 'usermeta';
423 }
424
425 $taskDto->existingViews = [];
426 if ($this->renameViews) {
427 $taskDto->existingViews = $this->tableService->findViewsNamesStartWith($this->productionTablePrefix) ?: [];
428 }
429
430 $taskDto->conflictingTablesRenamed = 0;
431 $taskDto->nonConflictingTablesRenamed = 0;
432 $taskDto->customTablesRenamed = 0;
433 $this->setTaskDto($taskDto);
434
435 return $taskDto;
436 }
437
438
439
440
441
442 public function setTaskDto(RenameDatabaseTaskDto $taskDto)
443 {
444 $this->tablesBeingRenamed = [];
445 $this->tablesBeingRenamed['tables'] = $taskDto->tablesBeingRenamed ?: [];
446 $this->tablesBeingRenamed['views'] = $taskDto->viewsBeingRenamed ?: [];
447 $this->tablesBeingRenamed['custom'] = $taskDto->customTablesBeingRenamed ?: [];
448 $this->tablesBeingRenamed['all'] = array_merge($this->tablesBeingRenamed['tables'], $this->tablesBeingRenamed['views']);
449
450 $this->existingTables = [];
451 $this->existingTables['tables'] = $taskDto->existingTables ?: [];
452 $this->existingTables['views'] = $taskDto->existingViews ?: [];
453 $this->existingTables['all'] = array_merge($this->existingTables['tables'], $this->existingTables['views']);
454
455 $this->totalTables = count($this->tablesBeingRenamed['tables']) + count($this->tablesBeingRenamed['custom']);
456 $tmpDatabasePrefix = $this->tmpPrefix;
457
458 foreach ($this->tablesBeingRenamed as $viewsOrTables => $tableName) {
459 $this->tablesBeingRenamedUnprefixed[$viewsOrTables] = array_map(function ($tableName) use ($tmpDatabasePrefix) {
460 $tableName = $this->getFullNameTableFromShortName($tableName, $tmpDatabasePrefix);
461 return substr($tableName, strlen($tmpDatabasePrefix));
462 }, $this->tablesBeingRenamed[$viewsOrTables]);
463 }
464
465 $productionTablePrefix = $this->productionTablePrefix;
466 $isSubsiteRestore = $this->isRenamingForSubsite;
467 $baseProdTablePrefix = $this->productionTableBasePrefix;
468
469 foreach ($this->existingTables as $viewsOrTables => $tableName) {
470 $this->existingTablesUnprefixed[$viewsOrTables] = array_map(function ($tableName) use ($productionTablePrefix, $baseProdTablePrefix, $isSubsiteRestore) {
471 if ($isSubsiteRestore && in_array($tableName, [ $baseProdTablePrefix . 'users', $baseProdTablePrefix . 'usermeta' ])) {
472 return substr($tableName, strlen($baseProdTablePrefix));
473 }
474
475 return substr($tableName, strlen($productionTablePrefix));
476 }, $this->existingTables[$viewsOrTables]);
477 }
478
479 $this->conflictingTablesRenamed = (int)$taskDto->conflictingTablesRenamed;
480 $this->nonConflictingTablesRenamed = (int)$taskDto->nonConflictingTablesRenamed;
481 $this->customTablesRenamed = (int)$taskDto->customTablesRenamed;
482 }
483
484
485
486
487
488
489
490 public function getFullNameTableFromShortName(string $table, string $prefix): string
491 {
492 $shortTables = [];
493 if ($prefix === $this->tmpPrefix) {
494 $shortTables = $this->shortNamedTablesToRename;
495 } elseif ($prefix === $this->dropPrefix) {
496 $shortTables = $this->shortNamedTablesToDrop;
497 }
498
499 if (!array_key_exists($table, $shortTables)) {
500 return $table;
501 }
502
503 return $shortTables[$table];
504 }
505
506
507
508
509
510
511 public function getTableShortName(string $table, string $prefix)
512 {
513 $shortTables = [];
514 if ($prefix === $this->tmpPrefix) {
515 $shortTables = $this->shortNamedTablesToRename;
516 } elseif ($prefix === $this->dropPrefix) {
517 $shortTables = $this->shortNamedTablesToDrop;
518 }
519
520 return array_search($table, $shortTables);
521 }
522
523
524
525
526
527 public function renameConflictingTables(): bool
528 {
529 $conflictingTablesWithoutPrefix = array_values($this->getTablesThatExistInBothExistingAndTempUnprefixed());
530
531 if (empty($conflictingTablesWithoutPrefix)) {
532 return true;
533 }
534
535
536 if (count($conflictingTablesWithoutPrefix) <= $this->conflictingTablesRenamed) {
537 return true;
538 }
539
540 $database = $this->tableService->getDatabase();
541 $database->exec('START TRANSACTION;');
542
543 try {
544 for ($i = $this->conflictingTablesRenamed; $i < count($conflictingTablesWithoutPrefix); $i++) {
545 $conflictingTableWithoutPrefix = $conflictingTablesWithoutPrefix[$i];
546 if ($this->isExcludedTable($conflictingTableWithoutPrefix)) {
547 $this->tablesRenamed++;
548 $this->conflictingTablesRenamed++;
549 continue;
550 }
551
552 if ($this->isTableToPreserve($conflictingTableWithoutPrefix)) {
553 $this->tablesRenamed++;
554 $this->nonConflictingTablesRenamed++;
555 continue;
556 }
557
558 if ($this->isAlreadyRenamed($conflictingTableWithoutPrefix)) {
559 $this->tablesRenamed++;
560 $this->conflictingTablesRenamed++;
561 continue;
562 }
563
564 $currentTable = $this->getCurrentSiteTable($conflictingTableWithoutPrefix);
565 $tableToDrop = $this->getTableShortName($currentTable, $this->dropPrefix);
566 if ($tableToDrop === false) {
567 $tableToDrop = $this->dropPrefix . $conflictingTableWithoutPrefix;
568 }
569
570 if ($this->tableExists($currentTable)) {
571 $this->renameTableOrFail($currentTable, $tableToDrop);
572 }
573
574 $this->renameTable($conflictingTableWithoutPrefix, $this->conflictingTablesRenamed);
575
576 if ($this->isThresholdReached()) {
577 return false;
578 }
579 }
580 } finally {
581 $database->exec('COMMIT;');
582 }
583
584 return true;
585 }
586
587
588
589
590
591 public function renameNonConflictingTables(): bool
592 {
593 $nonConflictingTables = array_values($this->getTablesThatExistInTempButNotInSite());
594
595 if (empty($nonConflictingTables)) {
596 return true;
597 }
598
599
600 if (count($nonConflictingTables) <= $this->nonConflictingTablesRenamed) {
601 return true;
602 }
603
604 $database = $this->tableService->getDatabase();
605 $database->exec('START TRANSACTION;');
606
607 try {
608 for ($i = $this->nonConflictingTablesRenamed; $i < count($nonConflictingTables); $i++) {
609 $nonConflictingTable = $nonConflictingTables[$i];
610 if ($this->isExcludedTable($nonConflictingTable)) {
611 $this->tablesRenamed++;
612 $this->nonConflictingTablesRenamed++;
613 continue;
614 }
615
616 if ($this->isTableToPreserve($nonConflictingTable)) {
617 $this->tablesRenamed++;
618 $this->nonConflictingTablesRenamed++;
619 continue;
620 }
621
622 if ($this->isAlreadyRenamed($nonConflictingTable)) {
623 $this->tablesRenamed++;
624 $this->nonConflictingTablesRenamed++;
625 $this->isNonConflictingTablesRenamingTaskExecuted = true;
626 continue;
627 }
628
629 $this->renameTable($nonConflictingTable, $this->nonConflictingTablesRenamed);
630 $this->isNonConflictingTablesRenamingTaskExecuted = true;
631
632 if ($this->isThresholdReached()) {
633 return false;
634 }
635 }
636 } finally {
637 $database->exec('COMMIT;');
638 }
639
640 return true;
641 }
642
643
644
645
646 public function cleanTemporaryBackupTables(): bool
647 {
648
649 if ($this->nonConflictingTablesRenamed !== 0 || $this->conflictingTablesRenamed !== 0) {
650 return true;
651 }
652
653 $this->tablesToBeDropped = $this->tableService->findTableNamesStartWith($this->dropPrefix) ?: [];
654 $this->tablesRemainingToBeDropped = count($this->tablesToBeDropped);
655
656 $this->tableService->getDatabase()->exec('SET autocommit=0;');
657 $this->tableService->getDatabase()->exec('SET FOREIGN_KEY_CHECKS=0;');
658 $this->tableService->getDatabase()->exec('START TRANSACTION;');
659 foreach ($this->tablesToBeDropped as $table) {
660 $result = $this->tableService->getDatabase()->exec(sprintf(
661 "DROP TABLE `%s`;",
662 $table
663 ));
664
665
666 if ($result === false) {
667 $this->tableService->getDatabase()->exec('COMMIT;');
668 $this->tableService->getDatabase()->exec('SET autocommit=1;');
669 return false;
670 }
671
672 $this->tablesRemainingToBeDropped--;
673 }
674
675 $this->tableService->getDatabase()->exec('COMMIT;');
676 $this->tableService->getDatabase()->exec('SET autocommit=1;');
677 return true;
678 }
679
680
681
682
683 public function renameTablesToDrop()
684 {
685 foreach ($this->getTablesThatExistInSiteButNotInTemp() as $table) {
686 if ($this->isTableToPreserve($table) || $this->isExistingDestinationSubsiteTable($table)) {
687 continue;
688 }
689
690 $fullTableName = $this->productionTablePrefix . $table;
691 $tableToDrop = $this->getTableShortName($fullTableName, $this->dropPrefix);
692 if ($tableToDrop === false) {
693 $tableToDrop = $this->dropPrefix . $table;
694 }
695
696 $result = $this->tableService->getDatabase()->exec(sprintf(
697 "RENAME TABLE `%s` TO `%s`;",
698 $fullTableName,
699 $tableToDrop
700 ));
701
702 if ($result !== false) {
703 continue;
704 }
705
706 if ($this->logger instanceof Logger) {
707 $this->logger->warning(sprintf(
708 'DB Rename: Unable to move the table %s aside to %s. Error: %s. That table is not part of the backup and has been left in place.',
709 $fullTableName,
710 $tableToDrop,
711 $this->tableService->getLastWpdbError()
712 ));
713 }
714 }
715 }
716
717
718
719
720
721 public function renameCustomTables(): bool
722 {
723 $customTablesToRename = $this->tablesBeingRenamed['custom'];
724
725 if (empty($customTablesToRename)) {
726 return true;
727 }
728
729 foreach ($customTablesToRename as $tmpCustomTable) {
730 $customTable = substr($tmpCustomTable, strlen($this->customTableTmpPrefix));
731
732 $result = true;
733 if ($this->tableExists($customTable)) {
734 $result = $this->renameQuery($customTable, $this->dropPrefix . $customTable);
735 }
736
737 if ($result === false) {
738 throw new \RuntimeException("Unable to rename custom table {$customTable} to {$this->dropPrefix}{$customTable}.");
739 }
740
741 $result = $this->renameQuery($tmpCustomTable, $customTable);
742 if ($result === false) {
743 throw new \RuntimeException("Unable to rename custom table {$tmpCustomTable} to {$customTable}.");
744 }
745
746 $this->customTablesRenamed++;
747 if ($this->isThresholdReached()) {
748 return false;
749 }
750 }
751
752 return true;
753 }
754
755
756
757
758
759
760 public function getActivePluginsToPreserve(): string
761 {
762 $tmpOptionsTable = $this->tmpPrefix . 'options';
763 if (!$this->tableExists($tmpOptionsTable)) {
764 return '';
765 }
766
767 $productionOptionsTable = $this->productionTablePrefix . 'options';
768 $activePluginsToPreserve = $this->getOptionValue($tmpOptionsTable, self::OPTION_ACTIVE_PLUGINS);
769 $currentActivePlugins = $this->getOptionValue($productionOptionsTable, self::OPTION_ACTIVE_PLUGINS);
770
771 if (empty($activePluginsToPreserve)) {
772 return $activePluginsToPreserve;
773 }
774
775
776 $wpstgActivePlugins = $this->safeMaybeUnserialize($currentActivePlugins);
777
778 if (!is_array($wpstgActivePlugins)) {
779
780 $this->insertOrUpdateOptionValue($productionOptionsTable, self::OPTION_ACTIVE_PLUGINS . '_bak', $currentActivePlugins);
781 $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".';
782 $wpstgActivePlugins = [];
783 }
784
785 $wpstgActivePlugins = array_filter($wpstgActivePlugins, function ($pluginSlug) {
786 return is_string($pluginSlug) && strpos($pluginSlug, self::PLUGIN_BASE_SLUG) === 0;
787 });
788
789 $wpstgActivePlugins = serialize($wpstgActivePlugins);
790 $this->updateOptionValue($tmpOptionsTable, self::OPTION_ACTIVE_PLUGINS, $wpstgActivePlugins);
791 $this->updateOptionValue($productionOptionsTable, self::OPTION_ACTIVE_PLUGINS, $wpstgActivePlugins);
792
793 return $activePluginsToPreserve;
794 }
795
796
797
798
799
800
801 public function getActiveSitewidePluginsToPreserve(): string
802 {
803 $tmpSiteMetaTable = $this->tmpPrefix . 'sitemeta';
804 if (!$this->tableExists($tmpSiteMetaTable)) {
805 return '';
806 }
807
808 $productionSiteMetaTable = $this->productionTablePrefix . 'sitemeta';
809 $activePluginsToPreserve = $this->getNetworkOptionValue($tmpSiteMetaTable, self::OPTION_ACTIVE_SITEWIDE_PLUGINS);
810 $currentActivePlugins = $this->getNetworkOptionValue($productionSiteMetaTable, self::OPTION_ACTIVE_SITEWIDE_PLUGINS);
811
812
813 $wpstgActivePlugins = $this->safeMaybeUnserialize($currentActivePlugins);
814
815 if (!is_array($wpstgActivePlugins)) {
816
817 $this->insertOrUpdateNetworkOptionValue($productionSiteMetaTable, self::OPTION_ACTIVE_SITEWIDE_PLUGINS . '_bak', $currentActivePlugins);
818 $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".';
819 $wpstgActivePlugins = [];
820 }
821
822 $wpstgActivePlugins = array_filter($wpstgActivePlugins, function ($pluginSlug) {
823 return is_string($pluginSlug) && strpos($pluginSlug, self::PLUGIN_BASE_SLUG) === 0;
824 }, ARRAY_FILTER_USE_KEY);
825
826 $wpstgActivePlugins = serialize($wpstgActivePlugins);
827 $this->updateNetworkOptionValue($tmpSiteMetaTable, self::OPTION_ACTIVE_SITEWIDE_PLUGINS, $wpstgActivePlugins);
828 $this->updateNetworkOptionValue($productionSiteMetaTable, self::OPTION_ACTIVE_SITEWIDE_PLUGINS, $wpstgActivePlugins);
829
830 return $activePluginsToPreserve;
831 }
832
833
834
835
836
837
838
839 public function restorePreservedActivePlugins(string $activePlugins, string $activeWpstgPlugin, bool $isNetworkActivatedPlugin): bool
840 {
841 $productionOptionsTable = $this->productionTablePrefix . 'options';
842 $preservedPlugins = $this->safeMaybeUnserialize($activePlugins);
843
844 if ($isNetworkActivatedPlugin) {
845 if (!is_array($preservedPlugins)) {
846 $this->addRejectedActivePluginsError($activePlugins);
847 $preservedPlugins = [];
848 }
849
850 $preservedPlugins = array_values(array_filter($preservedPlugins, 'is_string'));
851
852 return $this->updateOptionValue($productionOptionsTable, self::OPTION_ACTIVE_PLUGINS, serialize($preservedPlugins));
853 }
854
855
856 if (!is_array($preservedPlugins)) {
857
858 $this->insertOrUpdateOptionValue($productionOptionsTable, self::OPTION_ACTIVE_PLUGINS . '_bak', $activePlugins);
859 $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".';
860 $preservedPlugins = [];
861 }
862
863 $preservedPlugins = array_filter($preservedPlugins, function ($pluginSlug) {
864 if (!is_string($pluginSlug)) {
865 return false;
866 }
867
868
869 if (strpos($pluginSlug, self::PLUGIN_BASE_SLUG) !== false) {
870 return false;
871 }
872
873 return true;
874 });
875
876
877 $preservedPlugins[] = $activeWpstgPlugin;
878 sort($preservedPlugins);
879
880 return $this->updateOptionValue($productionOptionsTable, self::OPTION_ACTIVE_PLUGINS, serialize($preservedPlugins));
881 }
882
883
884
885
886
887 protected function addRejectedActivePluginsError(string $originalValue)
888 {
889 if ($originalValue === '') {
890 return;
891 }
892
893 $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.';
894 }
895
896
897
898
899
900
901
902 public function restorePreservedActiveSitewidePlugins(string $activeSitewidePlugins, string $activeWpstgPlugin, $time = null): bool
903 {
904 $preservedSitewidePlugins = $this->safeMaybeUnserialize($activeSitewidePlugins);
905
906 if (!is_array($preservedSitewidePlugins)) {
907
908 $this->insertOrUpdateNetworkOptionValue($this->productionTablePrefix . 'sitemeta', self::OPTION_ACTIVE_SITEWIDE_PLUGINS . '_bak', $activeSitewidePlugins);
909 $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".';
910 $preservedSitewidePlugins = [];
911 }
912
913 $activeSitewidePlugins = array_filter($preservedSitewidePlugins, function ($pluginSlug) {
914
915
916 if (strpos($pluginSlug, self::PLUGIN_BASE_SLUG) !== false) {
917 return false;
918 }
919
920 return true;
921 });
922
923 if (!empty($activeWpstgPlugin)) {
924 $activeSitewidePlugins[$activeWpstgPlugin] = empty($time) ? time() : $time;
925 }
926
927 return $this->updateNetworkOptionValue($this->productionTablePrefix . 'sitemeta', self::OPTION_ACTIVE_SITEWIDE_PLUGINS, serialize($activeSitewidePlugins));
928 }
929
930 public function preserveTmpOption(string $optionName): bool
931 {
932 $tmpOptionsTable = $this->tmpPrefix . 'options';
933 if (!$this->tableExists($tmpOptionsTable)) {
934 return false;
935 }
936
937 $optionsTable = $this->productionTablePrefix . 'options';
938 $optionValue = $this->getOptionValue($optionsTable, $optionName);
939 if (empty($optionValue)) {
940 return false;
941 }
942
943 if ($this->getOptionValue($tmpOptionsTable, $optionName)) {
944 return $this->updateOptionValue($tmpOptionsTable, $optionName, $optionValue);
945 }
946
947 return $this->insertOptionValue($tmpOptionsTable, $optionName, $optionValue);
948 }
949
950
951
952
953
954 protected function isExcludedTable(string $tableName): bool
955 {
956 return in_array($tableName, $this->excludedTables);
957 }
958
959
960
961
962
963 protected function isTableToPreserve(string $tableName): bool
964 {
965 return in_array($tableName, $this->tablesToPreserve, true);
966 }
967
968
969
970
971
972 protected function isExistingDestinationSubsiteTable(string $tableName): bool
973 {
974 if (!$this->isRenamingForSubsite || strcasecmp($this->productionTablePrefix, $this->productionTableBasePrefix) !== 0) {
975 return false;
976 }
977
978 if (preg_match('/^(\d+)_/', $tableName, $matches) !== 1) {
979 return false;
980 }
981
982 $blogId = (int) $matches[1];
983 return (string) $blogId === $matches[1] && isset($this->destinationSubsiteBlogIds[$blogId]);
984 }
985
986
987
988
989 protected function getTablesThatExistInBothExistingAndTempUnprefixed(): array
990 {
991 return array_intersect($this->tablesBeingRenamedUnprefixed['all'], $this->existingTablesUnprefixed['all']);
992 }
993
994
995
996
997 protected function getTablesThatExistInSiteButNotInTemp(): array
998 {
999 return array_diff($this->existingTablesUnprefixed['all'], $this->tablesBeingRenamedUnprefixed['all']);
1000 }
1001
1002
1003
1004
1005 protected function getTablesThatExistInTempButNotInSite(): array
1006 {
1007 return array_diff($this->tablesBeingRenamedUnprefixed['all'], $this->existingTablesUnprefixed['all']);
1008 }
1009
1010
1011
1012
1013
1014
1015 protected function renameTable(string $tableWithoutPrefix, int &$tablesRenamed)
1016 {
1017 $tableToRename = $this->getTmpTable($tableWithoutPrefix);
1018 $tableAfterRenamed = $this->getCurrentSiteTable($tableWithoutPrefix);
1019
1020 if (!$this->renameTableOrFail($tableToRename, $tableAfterRenamed)) {
1021 return;
1022 }
1023
1024 $this->tablesRenamed++;
1025 $tablesRenamed++;
1026
1027 if ($this->logEachRename && $this->logger instanceof Logger) {
1028 $this->logger->info("DB Rename: Renamed table {$tableToRename} to {$tableAfterRenamed}.");
1029 }
1030 }
1031
1032
1033
1034
1035
1036
1037
1038 protected function renameTableOrFail(string $tableToRename, string $tableAfterRenamed): bool
1039 {
1040 $result = $this->tableService->getDatabase()->exec(sprintf(
1041 "RENAME TABLE `%s` TO `%s`;",
1042 $tableToRename,
1043 $tableAfterRenamed
1044 ));
1045
1046 if ($result !== false) {
1047 return true;
1048 }
1049
1050 $message = sprintf(
1051 'DB Rename: Unable to rename table %s to %s. Error: %s',
1052 $tableToRename,
1053 $tableAfterRenamed,
1054 $this->tableService->getLastWpdbError()
1055 );
1056
1057 if (!$this->stopOnRenameFailure) {
1058 if ($this->logEachRename && $this->logger instanceof Logger) {
1059 $this->logger->warning($message);
1060 }
1061
1062 return false;
1063 }
1064
1065 if ($this->logger instanceof Logger) {
1066 $this->logger->critical($message);
1067 }
1068
1069 throw new \RuntimeException($message);
1070 }
1071
1072
1073
1074
1075
1076 protected function isAlreadyRenamed(string $tableWithoutPrefix): bool
1077 {
1078 return !$this->tableExists($this->getTmpTable($tableWithoutPrefix));
1079 }
1080
1081
1082
1083
1084
1085 protected function getTmpTable(string $tableWithoutPrefix): string
1086 {
1087 $tmpTable = $this->tmpPrefix . $tableWithoutPrefix;
1088 $shortName = $this->getTableShortName($tmpTable, $this->tmpPrefix);
1089
1090 return $shortName === false ? $tmpTable : $shortName;
1091 }
1092
1093
1094
1095
1096
1097 protected function tableExists(string $tableName): bool
1098 {
1099 $database = $this->tableService->getDatabase()->getWpdba()->getClient();
1100 $result = $database->get_results(
1101 $database->prepare("SHOW TABLES LIKE %s", $database->esc_like($tableName)),
1102 ARRAY_A
1103 );
1104
1105 return !empty($result);
1106 }
1107
1108
1109
1110
1111
1112
1113 protected function getOptionValue(string $tableName, string $optionName): string
1114 {
1115 $database = $this->tableService->getDatabase()->getWpdba()->getClient();
1116 $result = $database->get_results(
1117 $database->prepare(
1118 "SELECT option_value FROM `{$tableName}` WHERE option_name LIKE %s",
1119 $database->esc_like($optionName)
1120 ),
1121 ARRAY_A
1122 );
1123 if (empty($result)) {
1124 return '';
1125 }
1126
1127 return $result[0]['option_value'];
1128 }
1129
1130
1131
1132
1133
1134
1135
1136 protected function updateOptionValue(string $tableName, string $optionName, string $optionValue): bool
1137 {
1138 $database = $this->tableService->getDatabase()->getWpdba()->getClient();
1139
1140 return $database->query(
1141 $database->prepare(
1142 "UPDATE `{$tableName}` SET option_value = %s WHERE option_name LIKE %s",
1143 $optionValue,
1144 $database->esc_like($optionName)
1145 )
1146 );
1147 }
1148
1149
1150
1151
1152
1153
1154
1155
1156 protected function insertOptionValue(string $tableName, string $optionName, string $optionValue, bool $autoload = false): bool
1157 {
1158 $database = $this->tableService->getDatabase()->getWpdba()->getClient();
1159
1160 return $database->query(
1161 $database->prepare(
1162 "INSERT INTO `{$tableName}` (option_name, option_value, autoload) VALUES (%s, %s, %s)",
1163 $optionName,
1164 $optionValue,
1165 $autoload ? 'yes' : 'no'
1166 )
1167 );
1168 }
1169
1170
1171
1172
1173
1174
1175
1176
1177 protected function insertOrUpdateOptionValue(string $tableName, string $optionName, string $optionValue, bool $autoload = false): bool
1178 {
1179 if ($this->getOptionValue($tableName, $optionName)) {
1180 return $this->updateOptionValue($tableName, $optionName, $optionValue);
1181 }
1182
1183 return $this->insertOptionValue($tableName, $optionName, $optionValue, $autoload);
1184 }
1185
1186
1187
1188
1189
1190
1191 protected function getNetworkOptionValue(string $tableName, string $optionName): string
1192 {
1193 $database = $this->tableService->getDatabase()->getWpdba()->getClient();
1194 $result = $database->get_results(
1195 $database->prepare(
1196 "SELECT meta_value FROM `{$tableName}` WHERE meta_key LIKE %s",
1197 $database->esc_like($optionName)
1198 ),
1199 ARRAY_A
1200 );
1201 if (empty($result)) {
1202 return '';
1203 }
1204
1205 return $result[0]['meta_value'];
1206 }
1207
1208
1209
1210
1211
1212
1213
1214 protected function insertNetworkOptionValue(string $tableName, string $optionName, string $optionValue): bool
1215 {
1216 $database = $this->tableService->getDatabase()->getWpdba()->getClient();
1217
1218 return $database->query(
1219 $database->prepare(
1220 "INSERT INTO `{$tableName}` (meta_key, meta_value) VALUES (%s, %s)",
1221 $optionName,
1222 $optionValue
1223 )
1224 );
1225 }
1226
1227
1228
1229
1230
1231
1232
1233 protected function updateNetworkOptionValue(string $tableName, string $optionName, string $optionValue): bool
1234 {
1235 $database = $this->tableService->getDatabase()->getWpdba()->getClient();
1236
1237 return $database->query(
1238 $database->prepare(
1239 "UPDATE `{$tableName}` SET meta_value = %s WHERE meta_key LIKE %s",
1240 $optionValue,
1241 $database->esc_like($optionName)
1242 )
1243 );
1244 }
1245
1246
1247
1248
1249
1250
1251
1252 protected function insertOrUpdateNetworkOptionValue(string $tableName, string $optionName, string $optionValue): bool
1253 {
1254 if ($this->getNetworkOptionValue($tableName, $optionName)) {
1255 return $this->updateNetworkOptionValue($tableName, $optionName, $optionValue);
1256 }
1257
1258 return $this->insertNetworkOptionValue($tableName, $optionName, $optionValue);
1259 }
1260
1261
1262
1263
1264 protected function isThresholdReached(): bool
1265 {
1266 if (!$this->phpAdapter->isCallable($this->thresholdCallable)) {
1267 return $this->customThreshold(false);
1268 }
1269
1270 $result = call_user_func($this->thresholdCallable);
1271 return $this->customThreshold($result);
1272 }
1273
1274
1275
1276
1277
1278 private function customThreshold(bool $isThreshold): bool
1279 {
1280 return Hooks::applyFilters('wpstg.tests.tablesRenamingThreshold', $isThreshold);
1281 }
1282
1283
1284
1285
1286
1287
1288 private function renameQuery(string $tableToRename, string $tableAfterRenamed): bool
1289 {
1290 $result = $this->tableService->renameTable($tableToRename, $tableAfterRenamed);
1291 if ($result !== false) {
1292 return true;
1293 }
1294
1295 if ($this->logEachRename && $this->logger instanceof Logger) {
1296
1297 $error = $this->tableService->getLastWpdbError();
1298 $this->logger->warning(sprintf("DB Rename: Unable to rename table %s to %s. Error: %s", $tableToRename, $tableAfterRenamed, $error));
1299 }
1300
1301 return false;
1302 }
1303
1304 private function getCurrentSiteTable(string $tableWithoutPrefix): string
1305 {
1306 if ($tableWithoutPrefix !== 'users' && $tableWithoutPrefix !== 'usermeta') {
1307 return $this->productionTablePrefix . $tableWithoutPrefix;
1308 }
1309
1310 if (!$this->isRenamingForSubsite) {
1311 return $this->productionTablePrefix . $tableWithoutPrefix;
1312 }
1313
1314 return $this->productionTableBasePrefix . $tableWithoutPrefix;
1315 }
1316 }
1317