PluginProbe ʕ •ᴥ•ʔ
Transferito: WP Migration / trunk
Transferito: WP Migration vtrunk
trunk 11.4.0 12.0.0 13.1.0 14.0.0 14.0.11 14.0.7 14.1.0 14.1.1 14.1.2 14.1.3 14.1.4
transferito / src / Models / Transfer / Database.php
transferito / src / Models / Transfer Last commit date
CodeBase.php 2 months ago Database.php 10 months ago Upload.php 8 months ago
Database.php
549 lines
1 <?php
2
3 namespace Transferito\Models\Transfer;
4
5 use mysqli;
6 use mysqli_driver;
7 use mysqli_sql_exception;
8
9 if (!defined('ABSPATH')) exit;
10
11 class Database {
12
13 private $collationTypes = array(
14 'column' => 'column',
15 'table' => 'table'
16 );
17
18 private function removeCollation($createTableSQL, $type)
19 {
20 if (!in_array($type, array_values($this->collationTypes))) {
21 return $createTableSQL;
22 }
23
24 $regExpPatterns = array(
25 'column' => '/( COLLATE)\s(.*?)(?=\s|,)/',
26 'table' => '/( COLLATE)=(.*?)$/'
27 );
28
29 $updatedSQL = preg_replace($regExpPatterns[$type], '', $createTableSQL);
30 return $updatedSQL;
31 }
32
33 public function moveDatabaseFiles()
34 {
35 /**
36 * Paths
37 */
38 $databaseDirectory = TRANSFERITO_UPLOAD_PATH . DIRECTORY_SEPARATOR . 'db_import';
39 $newDatabaseDirectory = TRANSFERITO_ABSPATH . 'transferito_import';
40
41 /**
42 * Command
43 */
44 $moveCommand = "mv {$databaseDirectory} {$newDatabaseDirectory}";
45
46 /**
47 * Save the PID
48 */
49 $pid = exec("{$moveCommand} > /dev/null & echo $!;");
50
51 /**
52 * Save the PID
53 */
54 set_transient('transferito_database_relocation_pid', $pid);
55 }
56
57 public function prepareTableMap()
58 {
59 $driver = new mysqli_driver();
60 $driver->report_mode = MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT;
61
62 try {
63 $mysqli = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
64 $mysqli->select_db(DB_NAME);
65 $queryTables = $mysqli->query('SHOW TABLES');
66 $tableMap = [];
67
68 /**
69 * Loop through the tables that have been found
70 */
71 while($row = $queryTables->fetch_row()) {
72 $foundTables[] = $row[0];
73 $table = $row[0];
74
75 /**
76 * Get the table status for the looping table
77 */
78 $tableStatus = $mysqli
79 ->query("SHOW TABLE STATUS WHERE name='{$table}'")
80 ->fetch_assoc();
81
82 /**
83 * Get the table row count for the looping table
84 */
85 $tableRowCount = $mysqli
86 ->query("SELECT COUNT(*) FROM {$table}")
87 ->fetch_row();
88
89 /**
90 * Map the table with the row amount and the row length
91 */
92 $tableMap[] = [
93 'name' => $table,
94 'rowAmount' => $tableRowCount[0],
95 'rowLength' => $tableStatus['Avg_row_length'],
96 'tableSize' => $tableStatus['Data_length']
97 ];
98 }
99
100 return $tableMap;
101 } catch (mysqli_sql_exception $e) {
102 return false;
103 }
104 }
105
106 public function saveDatabaseExportPart($fileIndex, $content)
107 {
108 $directory = TRANSFERITO_UPLOAD_PATH . DIRECTORY_SEPARATOR . 'db_import';
109 $filename = $directory . DIRECTORY_SEPARATOR . 'transferito_db_import_' . $fileIndex . '.sql';
110
111 /**
112 * Check that to the directory has been created
113 * If it hasn't then create it
114 */
115 if (!file_exists($directory)) {
116 wp_mkdir_p($directory);
117 }
118
119 return file_put_contents($filename, $content);
120 }
121
122 public function chunkedDBExport($fileIndex = 1, $startingRowIndex = 0, $startingTableIndex = 0)
123 {
124 $driver = new mysqli_driver();
125 $driver->report_mode = MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT;
126
127 try {
128 $cycleCount = 0;
129 $byteCount = 0;
130 $initialOffset = $startingRowIndex;
131 $recordCount = 0;
132 $initialOffsetBeenSet = false;
133 $tableMap = get_transient('transferito_database_table_map');
134 $transferDetail = get_transient('transferito_transfer_detail');
135
136 $oldURL = $transferDetail['fromUrl'];
137 $newURL = $transferDetail['newUrl'];
138
139 $parsedOldURL = wp_parse_url($oldURL);
140 $parsedNewURL = wp_parse_url($newURL);
141
142 $escapedOldHost = '/' . $parsedOldURL['host'];
143 $escapedNewHost = '/' . $parsedNewURL['host'];
144
145 $escapedOldHostEncoded = '%2F' . $parsedOldURL['host'];
146 $escapedNewHostEncoded = '%2F' . $parsedNewURL['host'];
147
148 $tableMapAmount = count($tableMap);
149
150 $mysqli = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME);
151 $mysqli->select_db(DB_NAME);
152
153 $databaseName = DB_NAME;
154 $charsetQuery = $mysqli->query(
155 "SELECT `DEFAULT_CHARACTER_SET_NAME` FROM information_schema.SCHEMATA WHERE schema_name ='{$databaseName}'"
156 )->fetch_row();
157
158 /**
159 * Check to see if the charset is available
160 */
161 if (!isset($charsetQuery[0])) {
162 throw new \Exception('We can not get your charset.');
163 }
164
165 $charset = $charsetQuery[0];
166
167 /**
168 * @todo Clean up and remove properly
169 */
170 set_transient('transferito_database_charset_info', [
171 'actualCharset' => $charset,
172 'configCharset' => DB_CHARSET,
173 ]);
174
175 $settings = get_option('transferito_settings_option');
176 $useDefaultCollation = isset($settings['transferito_use_default_collation'])
177 ? $settings['transferito_use_default_collation']
178 : false;
179
180 $sqlContent = "/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40101 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS */;\n/*!40101 SET SESSION sql_mode = 'ALLOW_INVALID_DATES' */;\n/*!40101 SET FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET NAMES utf8mb4 */;\n\n";
181
182 $exportResult = [];
183
184 for ($tableIndex = $startingTableIndex; $tableIndex < $tableMapAmount;) {
185 /**
186 * If the row is at 0
187 * Then print the create table SQL with the drop table statement
188 */
189 if ($startingRowIndex === 0) {
190 $createTable = $mysqli->query('SHOW CREATE TABLE ' . $tableMap[$tableIndex]['name'])->fetch_row();
191 $createTableSQL = $createTable[1];
192
193 if ($useDefaultCollation) {
194 $columnUpdatedSQL = $this->removeCollation($createTableSQL, 'column');
195 $tableUpdatedSQL = $this->removeCollation($columnUpdatedSQL, 'table');
196 $sqlContent .= "\n\nDROP TABLE IF EXISTS `{$tableMap[$tableIndex]['name']}`;\n" . $tableUpdatedSQL . ";\n\n";
197 } else {
198 $sqlContent .= "\n\nDROP TABLE IF EXISTS `{$tableMap[$tableIndex]['name']}`;\n" . $createTableSQL . ";\n\n";
199 }
200 }
201
202 /**
203 * Query to pull the row data from this table
204 */
205 if ($startingRowIndex === 0 || $initialOffset === 0) {
206 $command = "SELECT * FROM {$tableMap[$tableIndex]['name']} LIMIT 10000";
207 } else {
208 $command = "SELECT * FROM {$tableMap[$tableIndex]['name']} LIMIT 10000 OFFSET {$initialOffset}";
209 }
210
211 $rowData = $mysqli->query($command);
212
213 /**
214 * Loop through the rows based on the starting index
215 */
216 for($rowIndex = 0; $rowIndex < $rowData->num_rows;) {
217 $rowData->data_seek($rowIndex);
218 $row = $rowData->fetch_row();
219
220 /**
221 * Correct byte allocation
222 */
223 $byteCount = $byteCount + strlen(serialize($row));
224
225 /**
226 * Add the insert statement on cycle count or first row
227 */
228 if ($rowIndex == 0 || $cycleCount % 100 == 0 || $cycleCount == 0) {
229 $sqlContent .= "\nINSERT IGNORE INTO " . $tableMap[$tableIndex]['name']. " VALUES ";
230 }
231
232 /**
233 * Open the bracket for the row value
234 */
235 $sqlContent .= "\n(";
236
237 /**
238 * Loop through the columns for the row
239 */
240 for ($fieldIndex = 0; $fieldIndex < $rowData->field_count; $fieldIndex++) {
241
242
243 if (isset($row[$fieldIndex]) && $row[$fieldIndex]) {
244 /**
245 * Replace the URLs
246 */
247 if (strpos($row[$fieldIndex], $oldURL) !== false) {
248 $row[$fieldIndex] = str_replace($oldURL, $newURL, $row[$fieldIndex]);
249 }
250
251 /**
252 * Replace the escaped domain
253 */
254 if (strpos($row[$fieldIndex], $escapedOldHost) !== false) {
255 $row[$fieldIndex] = str_replace($escapedOldHost, $escapedNewHost, $row[$fieldIndex]);
256 }
257
258 /**
259 * Replace the escaped url encoded domain
260 */
261 if (strpos($row[$fieldIndex], $escapedOldHostEncoded) !== false) {
262 $row[$fieldIndex] = str_replace($escapedOldHostEncoded, $escapedNewHostEncoded, $row[$fieldIndex]);
263 }
264
265 /**
266 * Fix broken serialized data
267 */
268 if (substr($row[$fieldIndex], 0, 2) === 'a:') {
269 if (!@unserialize($row[$fieldIndex])) {
270 $fixedSerialization = preg_replace_callback(
271 '/s:([0-9]+):\"(.*?)\";/',
272 function ($matches) { return "s:".strlen($matches[2]).':"'.$matches[2].'";'; },
273 $row[$fieldIndex]
274 );
275 $row[$fieldIndex] = $fixedSerialization;
276 }
277 }
278
279 /**
280 * Add slashes to the field value
281 */
282 $row[$fieldIndex] = $mysqli->real_escape_string($row[$fieldIndex]);
283
284 }
285
286 /**
287 * Check the value exists for the column value
288 */
289 if (isset($row[$fieldIndex])) {
290 $sqlContent .= "'" . $row[$fieldIndex] . "'" ;
291 } else {
292 $sqlContent .= "''";
293 }
294
295 /**
296 * Adda a comma to every column for the value statement except the penultimate
297 */
298 if ($fieldIndex < ($rowData->field_count - 1)) {
299 $sqlContent.= ",";
300 }
301 }
302
303 /**
304 * Close the bracket for the row value
305 */
306 $sqlContent .=")";
307
308 /**
309 * Dependent on the cycle or the end of the row value
310 * Add a comma or end the statement
311 */
312 if ((($cycleCount + 1) %100 == 0 && $cycleCount != 0) || ($rowIndex + 1) == $rowData->num_rows) {
313 $sqlContent .= ";";
314 } else {
315 $sqlContent .= ",";
316 }
317 $cycleCount = $cycleCount + 1;
318
319 /**
320 * Increment the row index
321 */
322 $rowIndex++;
323
324 /**
325 * Records Count
326 */
327 $recordCount = $rowIndex;
328
329 /**
330 * Every 10000 break
331 */
332 if ($rowIndex != 0 && $rowIndex % 10000 == 0) {
333 /**
334 * Current row index
335 */
336 $initialOffset = $initialOffset + $recordCount;
337 $initialOffsetBeenSet = true;
338 $command = "SELECT * FROM {$tableMap[$tableIndex]['name']} LIMIT 10000 OFFSET {$initialOffset}";
339 $rowData = $mysqli->query($command);
340 $rowIndex = 0;
341 }
342
343 /**
344 * If the byte count is greater than or equal to LIMIT
345 */
346 if ($byteCount >= TRANSFERITO_DB_LIMIT) {
347
348 /**
349 * If initial offset has already been set
350 * Then do not assign it again
351 */
352 if (!$initialOffsetBeenSet) {
353 $initialOffset = $initialOffset + $recordCount;
354 }
355
356 /**
357 * Check the last character in the import
358 */
359 if (substr($sqlContent, -1) === ',') {
360 $sqlContent = substr($sqlContent, 0, -1);
361 $sqlContent .= ";";
362 }
363
364 /**
365 * Add to the end of every file
366 */
367 $sqlContent .= "\n\n/*!40101 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;";
368
369 /**
370 * Save the export file
371 */
372 $this->saveDatabaseExportPart($fileIndex, $sqlContent);
373
374 /**
375 * Then increment the file index once the file has been saved
376 */
377 $fileIndex++;
378
379 /**
380 * Set the transient DB Export Progress
381 */
382 set_transient('transferito_db_export_progress', [
383 'currentRowIndex' => $initialOffset,
384 'tableIndex' => $tableIndex,
385 'fileIndex' => $fileIndex
386 ]);
387
388 /**
389 * Set the export flag to true
390 * Which will notify the API of completion
391 */
392 $exportResult = [
393 'completed' => false,
394 'currentRowIndex' => $initialOffset,
395 'tableIndex' => $tableIndex,
396 'fileIndex' => $fileIndex,
397 ];
398
399 break;
400 }
401 }
402
403 /**
404 * If we've reached the byte count
405 * Go no further than this
406 */
407 if ($byteCount >= TRANSFERITO_DB_LIMIT) {
408 break;
409 }
410
411 /**
412 * Increment the table index
413 */
414 $tableIndex++;
415
416 /**
417 * Reset the starting row index
418 */
419 $startingRowIndex = 0;
420
421 /**
422 * Reset the offset
423 */
424 $initialOffset = 0;
425
426 /**
427 * Assign a falsey value to the been set flag to reset it
428 */
429 $initialOffsetBeenSet = false;
430
431 /**
432 * When the DB export has finished
433 */
434 if ($tableIndex === $tableMapAmount) {
435 /**
436 * Save the export file
437 */
438 $this->saveDatabaseExportPart($fileIndex, $sqlContent);
439
440 /**
441 * Set the export flag to true
442 * Which will notify the API of completion
443 */
444 $exportResult = [ 'completed' => true ];
445 }
446 }
447
448 return $exportResult;
449 } catch(mysqli_sql_exception $e) {
450 return false;
451 }
452 }
453
454 public function createFileList()
455 {
456 try {
457 $directory = TRANSFERITO_UPLOAD_PATH . DIRECTORY_SEPARATOR . 'db_import';
458 $fileIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory), \RecursiveIteratorIterator::CHILD_FIRST);
459 $files = array();
460 $byteCount = 0;
461 $fileSuffix = 1;
462
463 /**
464 * Create the directory to house the json files
465 */
466 $dbExportDirectory = $directory . DIRECTORY_SEPARATOR . 'json';
467 wp_mkdir_p($dbExportDirectory);
468
469 /**
470 * Check if the zip archive should be created
471 */
472 $isZipArchive = useZipArchive();
473
474 /**
475 * Loop through the files
476 */
477 foreach ($fileIterator as $file) {
478 if ($file->isDir()){
479 continue;
480 }
481
482 /**
483 * Assign name to variable
484 */
485 $filename = $file->getPathname();
486 $byteCount = $byteCount + $file->getSize();
487
488 /**
489 * Split the path to just get the filename
490 */
491 $updatedName = explode(DIRECTORY_SEPARATOR . 'db_import' . DIRECTORY_SEPARATOR, $filename);
492
493 /**
494 * Check that we're only checking SQL files
495 */
496 if (strpos($updatedName[1], '.sql') !== false) {
497 /**
498 * If the byte count is greater than the archive limit - split the file
499 */
500 if ($byteCount >= TRANSFERITO_DB_LIMIT) {
501 file_put_contents($dbExportDirectory . DIRECTORY_SEPARATOR . 'file_list_' . $fileSuffix . '.json', json_encode($files));
502
503 $byteCount = 0;
504 $fileSuffix++;
505
506 /**
507 * Destroy the file
508 */
509 unset($files);
510 $files = [];
511 }
512
513 /**
514 * Create array based paths
515 */
516 if ($isZipArchive) {
517 $files[] = [
518 'originalPath' => $filename,
519 'updatedPath' => str_replace("\\", "/", 'transferito_import' . DIRECTORY_SEPARATOR . $updatedName[1]),
520 ];
521 }
522
523 /**
524 * Create path based array
525 */
526 if (!$isZipArchive) {
527 $files['transferito_import' . DIRECTORY_SEPARATOR . $updatedName[1]] = $filename;
528 }
529 }
530 }
531
532 /**
533 * Create the last file
534 */
535 if (count($files) > 0) {
536 file_put_contents($dbExportDirectory . DIRECTORY_SEPARATOR . 'file_list_' . $fileSuffix . '.json', json_encode($files));
537 }
538
539 return [
540 'created' => true,
541 'amount' => $fileSuffix
542 ];
543
544 } catch (\Exception $exception) {
545 return false;
546 }
547 }
548 }
549