PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / trunk
WP STAGING – WordPress Backups, Restore, Migration & Clone vtrunk
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 / TableService.php
wp-staging / Framework / Database Last commit date
Exporter 3 days ago QueryBuilder 3 days ago CustomTable.php 3 days ago DbInfo.php 3 days ago ExcludedTables.php 3 days ago ExternalDatabaseConfiguration.php 3 days ago OptionPreservationHandler.php 3 days ago SearchReplace.php 3 days ago SelectedTables.php 3 days ago TableDto.php 3 days ago TableService.php 2 hours ago TablesRenamer.php 2 hours ago WpDbInfo.php 3 days ago WpOptionsInfo.php 3 days ago iDbInfo.php 2 years ago
TableService.php
841 lines
1 <?php
2
3
4
5
6 namespace WPStaging\Framework\Database;
7
8 use RuntimeException;
9 use UnexpectedValueException;
10 use WPStaging\Backup\Service\Database\DatabaseImporter;
11 use WPStaging\Framework\Adapter\Database;
12 use WPStaging\Framework\Collection\Collection;
13 use WPStaging\Framework\Utils\Strings;
14
15 class TableService
16 {
17
18 const DROP_LOCK_WAIT_SECONDS = 3;
19
20
21 const DROP_ATTEMPTS = 3;
22
23
24 private $database;
25
26
27 private $client;
28
29
30 private $shouldStop;
31
32
33 private $errors = [];
34
35
36 private $hasRefusedProductionTable = false;
37
38
39 private $strHelper;
40
41 private $isSqlLite = false;
42
43
44
45
46 public function __construct($database = null)
47 {
48 $this->database = $database ?: new Database();
49 $this->client = $this->database->getClient();
50 $this->strHelper = new Strings();
51
52 $this->isSqlLite = property_exists($this->client, 'isSQLite');
53 }
54
55
56
57
58 public function getErrors()
59 {
60 return $this->errors;
61 }
62
63
64
65
66 public function hasRefusedProductionTable(): bool
67 {
68 return $this->hasRefusedProductionTable;
69 }
70
71
72
73
74 public function getShouldStop()
75 {
76 return $this->shouldStop;
77 }
78
79
80
81
82
83 public function setShouldStop($shouldStop = null)
84 {
85 $this->shouldStop = $shouldStop;
86 return $this;
87 }
88
89
90
91
92
93 public function tableExists(string $tableName): bool
94 {
95 $wpdb = $this->database->getWpdb();
96 $tables = $wpdb->get_results(
97 $wpdb->prepare('SHOW TABLES LIKE %s;', $wpdb->esc_like($tableName)),
98 ARRAY_A
99 );
100
101 if (!$tables) {
102 return false;
103 }
104
105 return true;
106 }
107
108
109
110
111
112
113 public function findAllTableStatus()
114 {
115 $tables = $this->database->find("SHOW TABLE STATUS");
116 if (!$tables) {
117 return null;
118 }
119
120 $collection = new Collection(TableDto::class);
121 foreach ($tables as $table) {
122 $collection->attach((new TableDto())->hydrate((array) $table));
123 }
124
125 return $collection;
126 }
127
128
129
130
131
132
133
134 public function findTableStatusStartsWith($prefix = null)
135 {
136
137 $tables = $this->database->find("SHOW TABLE STATUS LIKE '{$this->database->escapeSqlPrefixForLIKE($prefix)}%'");
138 if (!$tables) {
139 return null;
140 }
141
142 $collection = new Collection(TableDto::class);
143 foreach ($tables as $table) {
144 $collection->attach((new TableDto())->hydrate((array) $table));
145 }
146
147 return $collection;
148 }
149
150
151
152
153
154
155
156 public function getTablesName($tables): array
157 {
158 return (!is_array($tables)) ? [] : array_map(function ($table) {
159 return ($table->getName());
160 }, $tables);
161 }
162
163
164
165
166
167
168
169
170 public function findTableNamesStartWith(string $prefix = ''): array
171 {
172 $query = $this->getTablesFindQueryByTableType('BASE TABLE', $prefix);
173 $result = $this->client->query($query);
174 if (!$result) {
175 return [];
176 }
177
178 $tables = [];
179 while ($row = $this->client->fetchRow($result)) {
180 if (isset($row[0])) {
181 $tables[] = $row[0];
182 }
183 }
184
185 $this->client->freeResult($result);
186
187 return $tables;
188 }
189
190
191
192
193
194
195
196
197 public function findViewsNamesStartWith(string $prefix = ''): array
198 {
199 $query = $this->getTablesFindQueryByTableType('VIEW', $prefix);
200 $result = $this->client->query($query);
201 if (!$result) {
202 return [];
203 }
204
205 $views = [];
206 while ($row = $this->client->fetchRow($result)) {
207 if (isset($row[0])) {
208 $views[] = $row[0];
209 }
210 }
211
212 $this->client->freeResult($result);
213
214 return $views;
215 }
216
217
218
219
220
221
222 public function getCreateViewQuery(string $viewName): string
223 {
224 $result = $this->client->query("SHOW CREATE VIEW `{$viewName}`");
225 $row = $this->client->fetchAssoc($result);
226
227 $this->client->freeResult($result);
228
229 if (isset($row['Create View'])) {
230 return $row['Create View'];
231 }
232
233 return '';
234 }
235
236
237
238
239
240
241
242
243 public function getCreateTableQuery(string $tableName): string
244 {
245 $result = $this->client->query("SHOW CREATE TABLE `{$tableName}`");
246 if ($result === false) {
247 return '';
248 }
249
250 $row = $this->client->fetchAssoc($result);
251
252 $this->client->freeResult($result);
253
254 if (isset($row['Create Table'])) {
255 return $row['Create Table'];
256 }
257
258 return '';
259 }
260
261
262
263
264
265
266
267
268
269 public function deleteTablesStartWith(string $prefix, array $excludedTables = [], bool $deleteViews = false): bool
270 {
271 if ($deleteViews) {
272
273 $views = $this->findViewsNamesStartWith($prefix);
274 if (is_array($views) && !empty($views)) {
275 $viewsToRemove = array_diff($views, $excludedTables);
276 if (!$this->deleteViews($viewsToRemove)) {
277 return false;
278 }
279 }
280 }
281
282 $tables = $this->findTableStatusStartsWith($prefix);
283 if ($tables === null) {
284 return true;
285 }
286
287 $tables = $this->getTablesName($tables->toArray());
288
289 $tablesToRemove = array_diff($tables, $excludedTables);
290 if ($tablesToRemove === []) {
291 return true;
292 }
293
294 if (!$this->deleteTables($tablesToRemove)) {
295 return false;
296 }
297
298 return true;
299 }
300
301
302
303
304
305
306
307 public function deleteTables($tables): bool
308 {
309 $isForeignKeyCheckEnabled = "0";
310
311 $result = $this->client->fetchAssoc($this->client->query("SELECT @@FOREIGN_KEY_CHECKS AS fk_check"));
312 if (!empty($result)) {
313 $isForeignKeyCheckEnabled = empty($result['fk_check']) ? "0" : $result['fk_check'];
314 }
315
316 if ($isForeignKeyCheckEnabled === "1") {
317 $this->client->query("SET FOREIGN_KEY_CHECKS = 0");
318 }
319
320 $lockWaitTimeout = $this->boundLockWait();
321
322 try {
323 return $this->dropTables($tables);
324 } finally {
325 $this->restoreLockWait($lockWaitTimeout);
326 if ($isForeignKeyCheckEnabled === "1") {
327 $this->client->query("SET FOREIGN_KEY_CHECKS = 1");
328 }
329 }
330 }
331
332
333
334
335 private function boundLockWait(): string
336 {
337 if ($this->isSqlLite) {
338 return '';
339 }
340
341 $lockWait = $this->client->query("SELECT @@SESSION.lock_wait_timeout AS lock_wait");
342 if ($lockWait === false) {
343 return '';
344 }
345
346 $result = $this->client->fetchAssoc($lockWait);
347 if (empty($result['lock_wait'])) {
348 return '';
349 }
350
351 $this->client->query("SET SESSION lock_wait_timeout = " . self::DROP_LOCK_WAIT_SECONDS);
352
353 return (string)$result['lock_wait'];
354 }
355
356
357
358
359
360 private function restoreLockWait(string $lockWaitTimeout)
361 {
362 if ($lockWaitTimeout === '') {
363 return;
364 }
365
366 $this->client->query("SET SESSION lock_wait_timeout = " . (int)$lockWaitTimeout);
367 }
368
369
370
371
372
373 private function dropTables(array $tables): bool
374 {
375 $isDeleted = true;
376 foreach ($tables as $table) {
377
378 if ($this->isProductionSiteTableOrView($table)) {
379 $this->errors[] = sprintf(__("Fatal Error: Trying to delete table %s of main WP installation!", 'wp-staging'), $table);
380 $this->hasRefusedProductionTable = true;
381
382 return false;
383 }
384
385 if ($this->dropTableWithRetries($table)) {
386 continue;
387 }
388
389 $isDeleted = false;
390 }
391
392 return $isDeleted;
393 }
394
395
396
397
398
399
400
401 private function dropTableWithRetries(string $table): bool
402 {
403 $lastError = '';
404 for ($attempt = 1; $attempt <= self::DROP_ATTEMPTS; $attempt++) {
405 if ($this->client->query("DROP TABLE `{$table}`;") !== false) {
406 return true;
407 }
408
409 $lastError = $this->client->error();
410 }
411
412 $this->errors[] = sprintf(
413 'Could not delete the table %s after %d attempts. Error: %s',
414 $table,
415 self::DROP_ATTEMPTS,
416 $lastError
417 );
418
419 return false;
420 }
421
422
423
424
425
426
427
428 public function deleteViews($views): bool
429 {
430 $lockWaitTimeout = $this->boundLockWait();
431
432 try {
433 return $this->dropViews($views);
434 } finally {
435 $this->restoreLockWait($lockWaitTimeout);
436 }
437 }
438
439
440
441
442
443 private function dropViews(array $views): bool
444 {
445 $isDeleted = true;
446 foreach ($views as $view) {
447
448 if ($this->isProductionSiteTableOrView($view)) {
449 $this->errors[] = sprintf(__("Fatal Error: Trying to delete view %s of main WP installation!", 'wp-staging'), $view);
450 $this->hasRefusedProductionTable = true;
451
452 return false;
453 }
454
455 if ($this->database->getWpdba()->exec("DROP VIEW {$view};") !== false) {
456 continue;
457 }
458
459 $this->errors[] = sprintf('Could not delete the view %s. Error: %s', $view, $this->getLastWpdbError());
460 $isDeleted = false;
461 }
462
463 return $isDeleted;
464 }
465
466
467
468
469 public function getDatabase()
470 {
471 return $this->database;
472 }
473
474
475
476
477
478 public function dropTablesLike(string $likeCondition): bool
479 {
480 $wpdb = $this->database->getWpdb();
481 $tables = $wpdb->get_results(
482 $wpdb->prepare('SHOW TABLES LIKE %s;', $wpdb->esc_like($likeCondition) . '%')
483 );
484
485 if (!$tables) {
486 return false;
487 }
488
489 foreach ($tables as $tableObj) {
490 $tableName = current($tableObj);
491 $wpdb->query("DROP TABLE IF EXISTS `$tableName`");
492 }
493
494 return true;
495 }
496
497
498
499
500
501 public function dropTable(string $tableName): bool
502 {
503 $wpdb = $this->database->getWpdb();
504 $tables = $wpdb->get_results(
505 $wpdb->prepare('SHOW TABLES LIKE %s;', $wpdb->esc_like($tableName)),
506 ARRAY_A
507 );
508
509 if (!$tables) {
510 return true;
511 }
512
513 foreach ($tables as $tableObj) {
514 $tableName = current($tableObj);
515 $wpdb->query("DROP TABLE IF EXISTS `$tableName`");
516 }
517
518 return true;
519 }
520
521
522
523
524
525
526 public function renameTable(string $sourceTable, string $destinationTable): bool
527 {
528
529 $result = $this->client->query(sprintf(
530 "RENAME TABLE `%s` TO `%s`;",
531 $sourceTable,
532 $destinationTable
533 ));
534
535 return $result !== false;
536 }
537
538
539
540
541
542
543 public function cloneTableWithoutData(string $sourceTable, string $destinationTable): bool
544 {
545 return $this->client->query("CREATE TABLE $destinationTable LIKE $sourceTable");
546 }
547
548
549
550
551
552
553
554
555 public function copyTableData(string $sourceTable, string $destinationTable, int $offset = 0, int $limit = 0): bool
556 {
557 $query = sprintf(
558 "INSERT INTO %s SELECT * FROM %s LIMIT %d OFFSET %d",
559 $destinationTable,
560 $sourceTable,
561 $limit,
562 $offset
563 );
564
565 return $this->client->query($query);
566 }
567
568
569
570
571
572 public function getRowsCount(string $tableName, bool $encapsulateTableName = true): int
573 {
574 $tableName = $encapsulateTableName ? "`$tableName`" : $tableName;
575
576 return (int)$this->database->getWpdb()->get_var("SELECT COUNT(1) FROM $tableName");
577 }
578
579
580
581
582 public function getLastWpdbError(): string
583 {
584
585 $wpdb = $this->database->getWpdba()->getClient();
586
587 return $wpdb->last_error;
588 }
589
590
591
592
593
594 public function getNumericPrimaryKey(string $database, string $table): string
595 {
596 if ($this->hasMoreThanOnePrimaryKey($database, $table)) {
597 throw new UnexpectedValueException();
598 }
599
600 $query = "SELECT COLUMN_NAME
601 FROM INFORMATION_SCHEMA.COLUMNS
602 WHERE TABLE_NAME = '$table'
603 AND TABLE_SCHEMA = '$database'
604 AND IS_NULLABLE = 'NO'
605 AND DATA_TYPE IN ('int', 'bigint', 'smallint', 'mediumint')
606 AND COLUMN_KEY = 'PRI'
607 AND EXTRA like '%auto_increment%';";
608
609 $result = $this->client->query($query);
610
611 if (!$result) {
612 throw new UnexpectedValueException();
613 }
614
615 $primaryKey = $this->client->fetchObject($result);
616
617 $this->client->freeResult($result);
618
619 if (!is_object($primaryKey)) {
620 throw new UnexpectedValueException();
621 }
622
623 if (!property_exists($primaryKey, 'COLUMN_NAME')) {
624 throw new UnexpectedValueException();
625 }
626
627 if (empty($primaryKey->COLUMN_NAME)) {
628 throw new UnexpectedValueException();
629 }
630
631 return $primaryKey->COLUMN_NAME;
632 }
633
634
635
636
637
638
639
640
641 public function replaceTableConstraints(string $input): string
642 {
643 $pattern = [
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665 '/(,)?(\s+)?CONSTRAINT\s(.*)\sREFERENCES\s(.*)(,)?(\s+)?ON\s+(DELETE|UPDATE)\s(.*)\s?(CASCADE|RESTRICT|NO\sACTION|SET\sNULL|SET\sDEFAULT)(,)/i',
666 '/(,)?(\s+)?CONSTRAINT\s(.*)\sREFERENCES\s(.*)(,)?(\s+)?ON\s+(DELETE|UPDATE)\s(.*)\s?\)/i',
667 '/\s+CONSTRAINT(.+)REFERENCES(.+),/i',
668 '/,\s+CONSTRAINT(.+)REFERENCES(.+)/i',
669 ];
670
671 $replace = ['', ')', '', ''];
672 return (string)preg_replace($pattern, $replace, $input);
673 }
674
675
676
677
678
679
680 public function replaceTableOptions(string $input): string
681 {
682 $search = [
683 'TYPE=InnoDB',
684 'TYPE=MyISAM',
685 'ENGINE=Aria',
686 'TRANSACTIONAL=0',
687 'TRANSACTIONAL=1',
688 'PAGE_CHECKSUM=0',
689 'PAGE_CHECKSUM=1',
690 'TABLE_CHECKSUM=0',
691 'TABLE_CHECKSUM=1',
692 'ROW_FORMAT=PAGE',
693 'ROW_FORMAT=FIXED',
694 'ROW_FORMAT=DYNAMIC',
695 ];
696 $replace = [
697 'ENGINE=InnoDB',
698 'ENGINE=MyISAM',
699 'ENGINE=MyISAM',
700 '',
701 '',
702 '',
703 '',
704 '',
705 '',
706 '',
707 '',
708 '',
709 ];
710
711 return str_ireplace($search, $replace, $input);
712 }
713
714
715
716
717
718 public function lockTable(string $tableName)
719 {
720 if (!$this->client->query("LOCK TABLES `$tableName` WRITE;")) {
721 throw new RuntimeException("WP STAGING: Could not lock table $tableName");
722 }
723 }
724
725
726
727
728
729 public function unlockTables()
730 {
731 if (!$this->client->query("UNLOCK TABLES;")) {
732 throw new RuntimeException("WP STAGING: Could not unlock tables");
733 }
734 }
735
736
737
738
739
740
741 public function getColumnTypes(string $tableName): array
742 {
743 $column_types = [];
744
745 $result = $this->client->query("SHOW COLUMNS FROM `{$tableName}`");
746 while ($row = $this->client->fetchAssoc($result)) {
747 if (isset($row['Field'])) {
748 $column_types[strtolower($row['Field'])] = strtolower($row['Type']);
749 }
750 }
751
752 $this->client->freeResult($result);
753
754 return $column_types;
755 }
756
757
758
759
760
761 private function isProductionSiteTableOrView($tableOrView): bool
762 {
763
764 if ($this->database->isExternal()) {
765 return false;
766 }
767
768 $productionPrefix = $this->database->getProductionPrefix();
769
770
771 $result = $this->strHelper->startsWith($tableOrView, $productionPrefix);
772 if (!$result) {
773 return false;
774 }
775
776 $tmpPrefixes = [
777 DatabaseImporter::TMP_DATABASE_PREFIX,
778 DatabaseImporter::TMP_DATABASE_PREFIX_TO_DROP,
779 ];
780
781 if (in_array($productionPrefix, $tmpPrefixes)) {
782 return true;
783 }
784
785 foreach ($tmpPrefixes as $tmpPrefix) {
786 if ($this->strHelper->startsWith($tableOrView, $tmpPrefix) && $this->strHelper->startsWith($tmpPrefix, $productionPrefix)) {
787 return false;
788 }
789 }
790
791 return true;
792 }
793
794
795
796
797
798
799 private function getTablesFindQueryByTableType(string $tableType, string $prefix = ''): string
800 {
801
802 if ($this->isSqlLite) {
803
804 $tableType = $tableType === 'VIEW' ? 'view' : 'table';
805 $query = "SELECT name FROM sqlite_master WHERE type = '{$tableType}'";
806 if (!empty($prefix)) {
807 $query .= " AND name LIKE '{$this->database->escapeSqlPrefixForLIKE($prefix)}%'";
808 }
809 } else {
810
811 $dbname = $this->database->getWpdba()->getClient()->dbname;
812 $query = "SHOW FULL TABLES FROM `{$dbname}` WHERE `Table_type` = '{$tableType}'";
813 if (!empty($prefix)) {
814 $query .= " AND `Tables_in_{$dbname}` LIKE '{$this->database->escapeSqlPrefixForLIKE($prefix)}%'";
815 }
816 }
817
818 return $query;
819 }
820
821
822
823
824
825
826 private function hasMoreThanOnePrimaryKey(string $database, string $table): bool
827 {
828 $query = "SHOW KEYS FROM $table WHERE Key_name = 'PRIMARY'";
829
830 $result = $this->client->query($query);
831
832 if (!$result) {
833 throw new UnexpectedValueException();
834 }
835
836 $primaryKeys = $this->client->fetchAll($result);
837
838 return count($primaryKeys) > 1;
839 }
840 }
841