PluginProbe ʕ •ᴥ•ʔ
Backup Migration / 2.1.2
Backup Migration v2.1.2
2.1.6 2.1.5.2 trunk 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.6.1 1.4.7 1.4.8 1.4.9 1.4.9.1 2.0.0 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.5.1
backup-backup / includes / database / better-backup-v3.php
backup-backup / includes / database Last commit date
export 3 months ago better-backup-v3.php 3 months ago better-backup.php 3 months ago better-restore.php 3 months ago even-better-restore-v3.php 3 months ago even-better-restore-v4.php 3 months ago interface-search-replace-repository.php 3 months ago manager.php 3 months ago search-replace-processor.php 3 months ago search-replace-repository.php 3 months ago search-replace-stack-based.php 3 months ago search-replace-v2.php 3 months ago search-replace.php 3 months ago smart-sort.php 3 months ago
better-backup-v3.php
655 lines
1 <?php
2
3 /**
4 * Author: Mikołaj `iClyde` Chodorowski
5 * Contact: kontakt@iclyde.pl
6 * Package: Backup Migration – WP Plugin
7 */
8
9 // Namespace
10 namespace BMI\Plugin\Database;
11
12 // Use
13 use BMI\Plugin\BMI_Logger AS Logger;
14 use BMI\Plugin\Progress\BMI_ZipProgress AS Progress;
15 use BMI\Plugin\Dashboard AS Dashboard;
16 use BMI\Plugin\Staging\BMI_Staging as Staging;
17 use BMI\Plugin\Backup_Migration_Plugin as BMP;
18
19 // Exit on direct access
20 if (!defined('ABSPATH')) exit;
21
22 // echo "Memory usage at the beginning: " . (memory_get_usage() / 1024 / 1024) . " MB \n";
23 // function bmi_find_wordpress_base_path() {
24 //
25 // $dir = dirname(__FILE__);
26 // $previous = null;
27 //
28 // do {
29 //
30 // if (file_exists($dir . '/wp-config.php')) return $dir;
31 // if ($previous == $dir) break;
32 // $previous = $dir;
33 //
34 // } while ($dir = dirname($dir));
35 //
36 // return null;
37 //
38 // }
39 //
40 // define('BASE_PATH', bmi_find_wordpress_base_path() . '/');
41 // define('WP_USE_THEMES', false);
42 //
43 // // Use WP Globals and load WordPress
44 // global $wp, $wp_query, $wp_the_query, $wp_rewrite, $wp_did_header;
45 // require_once BASE_PATH . 'wp-load.php';
46 // echo "Memory usage after core load: " . (memory_get_usage() / 1024 / 1024) . " MB \n";
47 // ini_set('memory_limit', '2M');
48
49 /**
50 * Database exporting
51 * Main Class, requires $wpdb
52 */
53 class BMI_Database_Exporter {
54
55 /**
56 * Private local variables
57 */
58 private $total_tables = 0;
59 private $recipes = [];
60 private $tables_by_size = [];
61 public $total_queries = 0;
62 public $total_rows = 0;
63 public $total_size = 0;
64 public $files = [];
65
66 public $wpdb;
67 public $logger;
68 public $storage;
69 public $percentage;
70 public $max_rows;
71 public $max_query_size;
72 public $table_prefix;
73 public $init_start;
74 public $isPostRevisionsExcluded = false;
75
76 /**
77 * __construct - Initialization and logger resolver
78 *
79 * @return self
80 */
81 function __construct($storage, &$logger, $batcher = false, $backupStart = false) {
82
83 /**
84 * WP Global Database variable
85 */
86 global $wpdb;
87 $this->wpdb = &$wpdb;
88
89 /**
90 * Logger of BMI core
91 */
92 $this->logger = &$logger;
93
94 /**
95 * Storage directory
96 */
97 // $this->storage = trailingslashit(__DIR__) . 'data';
98 $this->storage = $storage;
99
100 /**
101 * Percentage escape to replace
102 * This way we know what the randomized string is
103 */
104 $this->percentage = trim($this->wpdb->prepare('%s', '%'), "'");
105
106 /**
107 * Max rows to pass each query
108 */
109 $this->max_rows = BMI_DB_MAX_ROWS_PER_QUERY;
110
111 /**
112 * Max size in bytes of single query export/import
113 */
114 $this->max_query_size = 1 * 1024 * 1024;
115
116 $this->table_prefix = time();
117 if ($backupStart && $backupStart !== false && is_numeric($backupStart)) {
118 $this->table_prefix = $backupStart;
119 }
120 $this->init_start = microtime(true);
121 if ($batcher === false || $batcher === 0) {
122 $this->logger->log("Memory usage after initialization: " . number_format(memory_get_usage() / 1024 / 1024, 2) . " MB", 'INFO');
123 }
124
125 $isSmartExclusion =defined("BMI_BACKUP_PRO") && BMI_BACKUP_PRO && Dashboard\bmi_get_config('SMART:EXCLUSION:ENABLED') == 'true' ? true : false;
126 $this->isPostRevisionsExcluded = $isSmartExclusion &&(Dashboard\bmi_get_config('SMART:EXCLUSION:PREVISIONS') == 'true' ? true : false);
127
128 }
129
130 /**
131 * export - Export initializer
132 *
133 * @return filename/filenames
134 */
135 public function export($batchingStep = false, $indexEnded = 0) {
136
137 // Table names
138 $this->get_table_names_and_sizes($batchingStep);
139 if ($batchingStep === false || $batchingStep === 0) {
140 $this->logger->log("Scan found $this->total_tables tables ($this->total_rows rows), estimated total size: $this->total_size MB.", 'INFO');
141 $this->logger->log("Memory usage after getting table names: " . number_format(memory_get_usage() / 1024 / 1024, 2) . " MB ", 'INFO');
142 }
143
144 // Recipes
145 if ($batchingStep === false || $batchingStep === 0) {
146 $this->logger->log("Getting table recipes...", 'INFO');
147 $this->table_recipes();
148 $this->logger->log("Table recipes have been exported.", 'INFO');
149 $this->logger->log("Memory usage after loading recipes: " . number_format(memory_get_usage() / 1024 / 1024, 2) . " MB ", 'INFO');
150 }
151
152 // Save Recipes
153 if ($batchingStep === false || $batchingStep === 0) {
154 $this->logger->log("Saving recipes...", 'INFO');
155 $this->save_recipes();
156 $this->logger->log("Recipes saved.", 'INFO');
157 $this->logger->log("Memory usage after recipe off-load: " . number_format(memory_get_usage() / 1024 / 1024, 2) . " MB", 'INFO');
158 }
159
160 // Tables data
161 if ($batchingStep === false || $batchingStep === 0) {
162 $this->logger->log("Exporting table data...", 'INFO');
163 }
164 $finishedAt = $this->get_tables_data($batchingStep, $indexEnded);
165 if ($batchingStep === false || $finishedAt['dumpCompleted'] === true) {
166 $this->logger->log("Table data exported.", 'INFO');
167 $this->logger->log("Memory usage after data export: " . number_format(memory_get_usage() / 1024 / 1024, 2) . " MB", 'INFO');
168 }
169
170 if ($batchingStep === false) {
171 $end = number_format(microtime(true) - $this->init_start, 4);
172 $this->logger->log("Entire process took: $end s", 'INFO');
173 }
174
175 return $finishedAt;
176
177 }
178
179 /**
180 * getPrefixesOfStagingSitesTables - Gets database prefixes of staging sites
181 *
182 * @return {array} of prefixes (can be empty)
183 */
184 private function getPrefixesOfStagingSitesTables() {
185
186 global $table_prefix;
187
188 require_once BMI_INCLUDES . '/staging/controller.php';
189 $staging = new Staging('..ajax..');
190 $stagingSites = $staging->getStagingSites(true);
191 $prefixes = [];
192
193 foreach ($stagingSites as $name => $data) {
194 if (!isset($data['db_prefix'])) continue;
195 if (!$data['db_prefix']) continue;
196 if (trim(strtolower($data['db_prefix'])) == '') continue;
197 if (trim(strtolower($data['db_prefix'])) == trim(strtolower($table_prefix))) continue;
198 if (isset($data['db_prefix'])) $prefixes[] = $data['db_prefix'];
199 }
200
201 return $prefixes;
202
203 }
204
205 /**
206 * checkIfTableIsPartOfStaging - Compares table prefix with staging site prefixes
207 *
208 * @return {bool} true/false
209 */
210 private function checkIfTableIsPartOfStaging(&$stagingSitePrefixes, $table) {
211
212 for ($i = 0; $i < sizeof($stagingSitePrefixes); ++$i) {
213 $prefix = $stagingSitePrefixes[$i];
214 if (substr($table, 0, strlen($prefix)) == $prefix) return true;
215 }
216
217 return false;
218
219 }
220
221 /**
222 * get_table_names_and_sizes - Gets table names and sizes
223 *
224 * @return {array} associative array table_name => [size => its size in MB, rows => rows count]
225 */
226 private function get_table_names_and_sizes($batchingStep) {
227
228 $tables = $this->wpdb->get_results('SHOW TABLES');
229 $shouldExcludeTables = Dashboard\bmi_get_config('BACKUP:DATABASE:EXCLUDE');
230 $stagingPrefixes = $this->getPrefixesOfStagingSitesTables();
231
232 $excludedTables = [];
233 $excludedTables = Dashboard\bmi_get_config('BACKUP:DATABASE:EXCLUDE:LIST');
234 if (!is_array($excludedTables) || empty($excludedTables)) $excludedTables = [];
235
236 foreach ($tables as $table_index => $table_object) {
237 foreach ($table_object as $database_name => $table_name) {
238
239 if (in_array($table_name, $excludedTables) && $shouldExcludeTables == 'true') {
240 $str = __('Excluding %s table from backup (due to exclusion rules).', 'backup-backup');
241 $str = str_replace('%s', $table_name, $str);
242 if ($batchingStep === false || intval($batchingStep) === 0) $this->logger->log($str, 'INFO');
243 continue;
244 }
245
246 if ($this->checkIfTableIsPartOfStaging($stagingPrefixes, $table_name)) {
247 $str = __('Excluding %s table from backup (as it is part of staging site).', 'backup-backup');
248 $str = str_replace('%s', $table_name, $str);
249 if ($batchingStep === false || intval($batchingStep) === 0) $this->logger->log($str, 'INFO');
250 continue;
251 }
252
253 $query = "SELECT table_name AS `table`, round(((data_length + index_length) / 1024 / 1024), 2) AS `size`, ";
254 $query .= "(SELECT COUNT(*) FROM " . BMP::escapeSQLIDentifier($table_name) . ") AS `rows`";
255 $query .= "FROM information_schema.TABLES ";
256 $query .= "WHERE table_schema = %s AND table_name = %s";
257
258 if ($this->isPostRevisionsExcluded && ($table_name == $this->wpdb->posts || $table_name == $this->wpdb->postmeta) && has_filter('bmip_smart_exclusion_post_revisions_count')) {
259 $query = apply_filters('bmip_smart_exclusion_post_revisions_count', $query, $table_name);
260 }
261
262 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
263 $results = $this->wpdb->get_results($this->wpdb->prepare($query, DB_NAME, $table_name));
264
265 if (!is_object($results[0])) {
266 if ($batchingStep === false || intval($batchingStep) === 0) {
267 $this->logger->log("Could not get info about: $table_name (#01)", 'WARN');
268 }
269 continue;
270 }
271
272 $table_name_returned = trim($results[0]->table);
273 if ($table_name != $table_name_returned || strlen(trim($table_name)) <= 0) {
274 if ($batchingStep === false || intval($batchingStep) === 0) {
275 $this->logger->log("Could not get info about: $table_name (#02)", 'WARN');
276 }
277 continue;
278 }
279
280 $this->tables_by_size[$table_name_returned] = array(
281 'size' => floatval($results[0]->size),
282 'rows' => intval($results[0]->rows)
283 );
284
285 $this->total_size += floatval($results[0]->size);
286 $this->total_rows += intval($results[0]->rows);
287 $this->total_tables++;
288
289 }
290 }
291
292 return $this->tables_by_size;
293
294 }
295
296 /**
297 * table_recipes - Gets CREATION recipe of each table
298 *
299 * @return {array} - Creation recipes for each table_name => recipe
300 */
301 private function table_recipes() {
302
303 foreach ($this->tables_by_size as $table_name => $table_object) {
304
305 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
306 $result = $this->wpdb->get_results("SHOW CREATE TABLE " . BMP::escapeSQLIDentifier($table_name) . ";");
307 foreach ($result as $index => $result_object) {
308 foreach ($result_object as $column_name => $column_value) {
309
310 if ($column_value == $table_name) continue;
311 else {
312
313 $column_value = str_replace(BMP::escapeSQLIDentifier($table_name), BMP::escapeSQLIDentifier($this->table_prefix . '_' . $table_name), $column_value);
314
315 $recipe = 'CREATE TABLE IF NOT EXISTS ';
316 $recipe .= substr($column_value, 13);
317 $recipe = str_replace("\n ", "", $recipe);
318 $recipe = str_replace("\n", "", $recipe);
319
320 $this->recipes[$table_name] = $recipe;
321
322 }
323
324 }
325 }
326
327 }
328
329 return $this->recipes;
330
331 }
332
333 /**
334 * save_recipes - Save recipes and off-load the memory
335 *
336 * @return {void}
337 */
338 private function save_recipes() {
339
340 $time_prefix = $this->table_prefix;
341 foreach ($this->recipes as $table_name => $table_recipe) {
342
343 $this->total_queries += 3;
344 $recipe = "/* CUSTOM VARS START */\n";
345 $recipe .= "/* REAL_TABLE_NAME: " . BMP::escapeSQLIDentifier($table_name) . "; */\n";
346 $recipe .= "/* PRE_TABLE_NAME: " . BMP::escapeSQLIDentifier($time_prefix . '_' . $table_name) . "; */\n";
347 $recipe .= "/* CUSTOM VARS END */\n\n";
348
349 $recipe .= $table_recipe . ";\n";
350
351 $this->total_rows++;
352 $location = $this->file_name($table_name);
353 $file = fopen($location, 'w');
354 fwrite($file, $recipe);
355
356 fclose($file);
357 unset($file);
358
359 $this->files[] = $location;
360 unset($location);
361
362 }
363
364 unset($this->recipes);
365
366 }
367
368 private function getArraySize(&$a, $baseSize = 0) {
369
370 $maxSize = $this->max_query_size;
371 $totalSize = 0 + $baseSize;
372 $i = 0;
373 $reachedLimit = false;
374
375 foreach ($a as $k => $v) {
376 if (is_object($v) || is_array($v)) {
377 $subSize = $this->getArraySize($v);
378 $size = $subSize['size'];
379 if (($totalSize + $size) > $maxSize && $totalSize != 0) {
380 $reachedLimit = true;
381 break;
382 } else {
383 $totalSize += $size;
384 $i++;
385 }
386 } else if (strval($v)) {
387 $totalSize += strlen($v);
388 }
389 }
390
391 return [
392 'size' => $totalSize,
393 'index' => $i,
394 'limit' => $reachedLimit
395 ];
396
397 }
398
399 /**
400 * get_tables_data - Table data getter
401 *
402 * @return {int} Total rows count
403 */
404 private function get_tables_data($batchingStep = false, $indexEnded = 0) {
405
406 $finishedAt = 0;
407 $currentTableIndex = 0;
408 $dumpCompleted = true;
409
410 foreach ($this->tables_by_size as $table_name => $table_object) {
411
412 $emptyTable = false;
413 $currentTableIndex = $currentTableIndex + 1;
414
415 if ($batchingStep !== false) {
416 if (intval($currentTableIndex - 1) !== intval($batchingStep)) {
417 continue;
418 } else {
419 $dumpCompleted = false;
420 }
421 }
422
423 $start_time = microtime(true);
424 if ($batchingStep === false || intval($indexEnded) === 0) {
425 $this->logger->log("Getting data of table: " . $table_name . " (" . $currentTableIndex . "/" . $this->total_tables . ", " . number_format($table_object['size'], 2) . " MB)", 'STEP');
426 }
427 $rows = intval($table_object['rows']);
428
429 $this->wpdb->query("SET foreign_key_checks = 0;");
430
431 $currentBufferSize = 0;
432 $bufferResult = [];
433
434 $i = 0;
435 if ($batchingStep !== false) $i = $indexEnded;
436
437 if (intval($table_object['rows']) > 0) {
438 for (;$i < $rows;) {
439
440 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
441 $query = $this->wpdb->prepare("SELECT * FROM " . BMP::escapeSQLIDentifier($table_name) . " LIMIT %d, $this->max_rows", $i);
442 if ($this->isPostRevisionsExcluded && ($table_name == $this->wpdb->posts || $table_name == $this->wpdb->postmeta) && has_filter('bmip_smart_exclusion_post_revisions')) {
443 $query = apply_filters('bmip_smart_exclusion_post_revisions', $query, $table_name, $i, $this->max_rows);
444 }
445 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above, may be modified by trusted filter
446 $result = $this->wpdb->get_results($query);
447
448 $valuesSize = $this->getArraySize($result, $currentBufferSize);
449 $rowsAmount = sizeof($result);
450 $valuesBytesSize = $valuesSize['size'] - $currentBufferSize;
451 $valuesMaxRow = $valuesSize['index'];
452 $valuesLimit = $valuesSize['limit'];
453
454 if ($valuesMaxRow < $rowsAmount && $valuesMaxRow != 0) $result = array_slice($result, 0, $valuesMaxRow);
455
456 $i += $valuesMaxRow;
457 $currentBufferSize += $valuesBytesSize;
458 $finishedAt = $i;
459
460 if ($valuesMaxRow != 0) $bufferResult = array_merge($bufferResult, $result);
461
462 if ($currentBufferSize >= $this->max_query_size || $i >= $rows || $valuesLimit == true) {
463
464 $currentBufferSize = 0;
465 $this->save_data($bufferResult, $table_name);
466 unset($bufferResult);
467 $bufferResult = [];
468
469 if ($batchingStep !== false) break;
470
471 }
472
473 unset($result);
474
475 }
476
477 $percentg = 100;
478 if (intval($table_object['rows']) !== 0 && is_numeric(intval($table_object['rows']))) {
479
480 $percentg = number_format(($i / intval($table_object['rows']) * 100), 2);
481
482 }
483
484 if ($i >= $rows && $batchingStep !== false) {
485
486 $batchingStep = $batchingStep + 1;
487 $finishedAt = 0;
488
489 $this->logger->log("Milestone of table " . $table_name . ": " . $i . "/" . $table_object['rows'] . " rows (" . $percentg . "%, " . number_format((microtime(true) - $start_time), 5) . "s)", 'INFO');
490 $this->logger->log("Table export for: " . $table_name . " finished", 'SUCCESS');
491
492 } else if ($batchingStep !== false) {
493
494 $this->logger->log("Milestone of table " . $table_name . ": " . $i . "/" . $table_object['rows'] . " rows (" . $percentg . "%, " . number_format((microtime(true) - $start_time), 5) . "s)", 'INFO');
495
496 }
497
498 $this->wpdb->query("SET foreign_key_checks = 1;");
499
500 if ($batchingStep === false) {
501
502 $this->logger->log("Table export for: " . $table_name . " finished (" . number_format((microtime(true) - $start_time), 5) . "s)", 'SUCCESS');
503
504 }
505
506 unset($start_time);
507
508 } else {
509
510 $this->logger->log("Table " . $table_name . " is empty, saving only recipe.", 'INFO');
511 $emptyTable = true;
512
513 if ($batchingStep !== false) {
514
515 $batchingStep = $batchingStep + 1;
516 $finishedAt = 0;
517
518 }
519
520 }
521
522 if ($batchingStep !== false && $emptyTable === false) break;
523
524 }
525
526 return [
527 'finishedQuery' => $finishedAt,
528 'batchingStep' => $batchingStep,
529 'dumpCompleted' => $dumpCompleted
530 ];
531
532 }
533
534 /**
535 * save_data - Saves table data/row as query
536 *
537 * @param {wpdb object} &$result Database query result
538 * @param {string} &$table_name Table name
539 * @return {void}
540 */
541 private function save_data(&$result, &$table_name) {
542
543 $columns_schema_added = false;
544 $file = fopen($this->file_name($table_name), 'a+');
545
546 $this->total_queries++;
547 $query = "INSERT INTO " . BMP::escapeSQLIDentifier($this->table_prefix . "_" . $table_name) . " ";
548
549 foreach ($result as $index => $result_object) {
550
551 $data_in_order = array();
552 $format_in_order = array();
553 $columns_in_order = array();
554
555 foreach ($result_object as $column_name => $value) {
556
557 $data_in_order[] = $value;
558 $columns_in_order[] = "`$column_name`";
559
560 if (is_numeric($value)) {
561
562 if (is_float($value + 0)) $format_in_order[] = '%f';
563 else $format_in_order[] = '%d';
564
565 } else if (gettype($value) == 'NULL') {
566
567 $format_in_order[] = '%null';
568
569 } else $format_in_order[] = '%s';
570
571 }
572
573 if ($columns_schema_added === false) {
574
575 $query .= "(" . implode(', ', $columns_in_order) . ") VALUES (";
576 $columns_schema_added = true;
577
578 } else {
579
580 $query = "),(";
581
582 }
583
584 $columns = sizeof($columns_in_order);
585 unset($columns_in_order);
586
587 // $query .= "/* VALUES START */\n";
588 for ($i = 0; $i < $columns; ++$i) {
589
590 if ($format_in_order[$i] == '%f') {
591
592 $floatresult = floatval($data_in_order[$i]);
593 if (!is_infinite($floatresult) && is_numeric($floatresult)) {
594 $query .= $floatresult;
595 } else {
596 if (defined('PHP_FLOAT_MAX')) $query .= PHP_INT_MAX;
597 else $query .= PHP_FLOAT_MAX;
598 }
599
600 } elseif ($format_in_order[$i] == '%d') {
601
602 $intresult = intval($data_in_order[$i]);
603 if (!is_infinite($intresult) && is_numeric($intresult)) {
604 $query .= $intresult;
605 } else {
606 $query .= PHP_INT_MAX;
607 }
608
609 } elseif ($format_in_order[$i] == '%null') {
610
611 $query .= 'NULL';
612
613 } else {
614
615 $query .= $this->wpdb->prepare("%s", $data_in_order[$i]);
616 $query = str_replace($this->percentage, '%', $query);
617
618 }
619
620 if ($i < ($columns - 1)) $query .= ",";
621
622 }
623
624 unset($data_in_order);
625 unset($format_in_order);
626 unset($columns_in_order);
627
628 fwrite($file, $query);
629
630 }
631
632 // fwrite($file, ");\n/* QUERY END */\n\n");
633 fwrite($file, ");\n");
634 fclose($file);
635 unset($file);
636
637 }
638
639 /**
640 * file_name - Replaces table name to file name friendly format
641 *
642 * @param {string} $table_name Table name
643 * @return {string} Friendly format for file
644 */
645 private function file_name($table_name) {
646
647 $friendly_name = preg_replace("/[^A-Za-z0-9_-]/", '', $table_name);
648 $friendly_name = trailingslashit($this->storage) . $friendly_name . '.sql';
649
650 return $friendly_name;
651
652 }
653
654 }
655