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