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