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