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.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.php
428 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\Backup_Migration_Plugin as BMP;
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 /**
66 * __construct - Initialization and logger resolver
67 *
68 * @return self
69 */
70 function __construct($storage, &$logger) {
71
72 /**
73 * WP Global Database variable
74 */
75 global $wpdb;
76 $this->wpdb = &$wpdb;
77
78 /**
79 * Logger of BMI core
80 */
81 $this->logger = &$logger;
82
83 /**
84 * Storage directory
85 */
86 // $this->storage = trailingslashit(__DIR__) . 'data';
87 $this->storage = $storage;
88
89 /**
90 * Percentage escape to replace
91 * This way we know what the randomized string is
92 */
93 $this->percentage = trim($this->wpdb->prepare('%s', '%'), "'");
94
95 /**
96 * Max rows to pass each query
97 */
98 $this->max_rows = BMI_DB_MAX_ROWS_PER_QUERY;
99
100 $this->table_prefix = time();
101 $this->init_start = microtime(true);
102 $this->logger->log("Memory usage after initialization: " . number_format(memory_get_usage() / 1024 / 1024, 2) . " MB", 'INFO');
103
104 }
105
106 /**
107 * export - Export initializer
108 *
109 * @return filename/filenames
110 */
111 public function export() {
112
113 // Table names
114 $this->get_table_names_and_sizes();
115 $this->logger->log("Scan found $this->total_tables tables ($this->total_rows rows), estimated total size: $this->total_size MB.", 'INFO');
116 $this->logger->log("Memory usage after getting table names: " . number_format(memory_get_usage() / 1024 / 1024, 2) . " MB ", 'INFO');
117
118 // Recipes
119 $this->logger->log("Getting table recipes...", 'INFO');
120 $this->table_recipes();
121 $this->logger->log("Table recipes have been exported.", 'INFO');
122 $this->logger->log("Memory usage after loading recipes: " . number_format(memory_get_usage() / 1024 / 1024, 2) . " MB ", 'INFO');
123
124 // Save Recipes
125 $this->logger->log("Saving recipes...", 'INFO');
126 $this->save_recipes();
127 $this->logger->log("Recipes saved.", 'INFO');
128 $this->logger->log("Memory usage after recipe off-load: " . number_format(memory_get_usage() / 1024 / 1024, 2) . " MB", 'INFO');
129
130 // Tables data
131 $this->logger->log("Exporting table data...", 'INFO');
132 $this->get_tables_data();
133 $this->logger->log("Table data exported.", 'INFO');
134 $this->logger->log("Memory usage after data export: " . number_format(memory_get_usage() / 1024 / 1024, 2) . " MB", 'INFO');
135
136 $end = number_format(microtime(true) - $this->init_start, 4);
137 $this->logger->log("Entire process took: $end s", 'INFO');
138
139 }
140
141 /**
142 * get_table_names_and_sizes - Gets table names and sizes
143 *
144 * @return {array} associative array table_name => [size => its size in MB, rows => rows count]
145 */
146 private function get_table_names_and_sizes() {
147
148 $tables = $this->wpdb->get_results('SHOW TABLES');
149 $shouldExcludeTables = Dashboard\bmi_get_config('BACKUP:DATABASE:EXCLUDE');
150
151 $excludedTables = [];
152 if (defined('BMI_BACKUP_PRO') && BMI_BACKUP_PRO == 1) {
153 $excludedTables = Dashboard\bmi_get_config('BACKUP:DATABASE:EXCLUDE:LIST');
154 if (!is_array($excludedTables) || empty($excludedTables)) $excludedTables = [];
155 }
156
157 foreach ($tables as $table_index => $table_object) {
158 foreach ($table_object as $database_name => $table_name) {
159
160 if (in_array($table_name, $excludedTables) && $shouldExcludeTables) {
161 $str = __('Excluding %s table from backup (due to exclusion rules).', 'backup-backup');
162 $str = str_replace('%s', $table_name, $str);
163 $this->logger->log($str, 'INFO');
164
165 continue;
166 }
167
168 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
169 $results = $this->wpdb->get_results($this->wpdb->prepare("
170 SELECT table_name AS `table`, round(((data_length + index_length) / 1024 / 1024), 2) AS `size`,
171 (SELECT COUNT(*) FROM " . BMP::escapeSQLIDentifier($table_name) . ") AS `rows`
172 FROM information_schema.TABLES
173 WHERE table_schema = %s AND table_name = %s;
174 ", DB_NAME, $table_name));
175
176 if (!is_object($results[0])) {
177 $this->logger->log("Could not get info about: $table_name (#01)", 'INFO');
178 continue;
179 }
180
181 $table_name_returned = trim($results[0]->table);
182 if ($table_name != $table_name_returned || strlen(trim($table_name)) <= 0) {
183 $this->logger->log("Could not get info about: $table_name (#02)", 'INFO');
184 continue;
185 }
186
187 $this->tables_by_size[$table_name_returned] = array(
188 'size' => floatval($results[0]->size),
189 'rows' => intval($results[0]->rows)
190 );
191
192 $this->total_size += floatval($results[0]->size);
193 $this->total_rows += intval($results[0]->rows);
194 $this->total_tables++;
195
196 }
197 }
198
199 return $this->tables_by_size;
200
201 }
202
203 /**
204 * table_recipes - Gets CREATION recipe of each table
205 *
206 * @return {array} - Creation recipes for each table_name => recipe
207 */
208 private function table_recipes() {
209
210 foreach ($this->tables_by_size as $table_name => $table_object) {
211
212 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
213 $result = $this->wpdb->get_results("SHOW CREATE TABLE " . BMP::escapeSQLIDentifier($table_name) . ";");
214 foreach ($result as $index => $result_object) {
215 foreach ($result_object as $column_name => $column_value) {
216
217 if ($column_value == $table_name) continue;
218 else {
219
220 $column_value = str_replace(BMP::escapeSQLIDentifier($table_name), BMP::escapeSQLIDentifier( $this->table_prefix . '_' . $table_name), $column_value);
221
222 $recipe = 'CREATE TABLE IF NOT EXISTS ';
223 $recipe .= substr($column_value, 13);
224
225 $this->recipes[$table_name] = $recipe;
226
227 }
228
229 }
230 }
231
232 }
233
234 return $this->recipes;
235
236 }
237
238 /**
239 * save_recipes - Save recipes and off-load the memory
240 *
241 * @return {void}
242 */
243 private function save_recipes() {
244
245 $time_prefix = $this->table_prefix;
246 foreach ($this->recipes as $table_name => $table_recipe) {
247
248 $this->total_queries += 4 + 3;
249 $recipe = "/* QUERY START */\n";
250 $recipe .= "SET foreign_key_checks = 0;\n";
251 $recipe .= "/* QUERY END */\n\n";
252
253 $recipe .= "/* QUERY START */\n";
254 $recipe .= "SET SQL_MODE = '';\n";
255 $recipe .= "/* QUERY END */\n\n";
256
257 $recipe .= "/* QUERY START */\n";
258 $recipe .= "SET time_zone = '+00:00';\n";
259 $recipe .= "/* QUERY END */\n\n";
260
261 $recipe .= "/* QUERY START */\n";
262 $recipe .= "SET NAMES 'utf8';\n";
263 $recipe .= "/* QUERY END */\n\n";
264
265 $recipe .= "/* CUSTOM VARS START */\n";
266 $recipe .= "/* REAL_TABLE_NAME: `$table_name`; */\n";
267 $recipe .= "/* PRE_TABLE_NAME: `$time_prefix" . "_" . "$table_name`; */\n";
268 $recipe .= "/* CUSTOM VARS END */\n\n";
269
270 $recipe .= "/* QUERY START */\n";
271 $recipe .= $table_recipe . ";\n";
272 $recipe .= "/* QUERY END */\n\n";
273
274 $this->total_rows++;
275 $location = $this->file_name($table_name);
276 $file = fopen($location, 'w');
277 fwrite($file, $recipe);
278
279 fclose($file);
280 unset($file);
281
282 $this->files[] = $location;
283 unset($location);
284
285 }
286
287 unset($this->recipes);
288
289 }
290
291 /**
292 * get_tables_data - Table data getter
293 *
294 * @return {int} Total rows count
295 */
296 private function get_tables_data() {
297
298 foreach ($this->tables_by_size as $table_name => $table_object) {
299
300 $start_time = microtime(true);
301 $this->logger->log("Getting data of table: " . $table_name . " (" . number_format ($table_object['size'], 2) . " MB)", 'STEP');
302 $rows = intval($table_object['rows']);
303
304 $this->wpdb->query("SET foreign_key_checks = 0;");
305
306 for ($i = 0; $i < $rows; $i += $this->max_rows) {
307
308 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
309 $result = $this->wpdb->get_results($this->wpdb->prepare("SELECT * FROM " . BMP::escapeSQLIDentifier($table_name) . " LIMIT %d, %d", $i, $this->max_rows));
310
311 $this->save_data($result, $table_name);
312 unset($result);
313
314 }
315
316 $this->wpdb->query("SET foreign_key_checks = 1;");
317
318 $this->logger->log("Table: " . $table_name . " cloned, operation took: " . number_format((microtime(true) - $start_time), 5) . " ms", 'INFO');
319 unset($start_time);
320
321 }
322
323 }
324
325 /**
326 * save_data - Saves table data/row as query
327 *
328 * @param {wpdb object} &$result Database query result
329 * @param {string} &$table_name Table name
330 * @return {void}
331 */
332 private function save_data(&$result, &$table_name) {
333
334 $columns_schema_added = false;
335 $file = fopen($this->file_name($table_name), 'a+');
336
337 $this->total_queries++;
338 $query = "/* QUERY START */\n";
339 $query .= "INSERT INTO " . BMP::escapeSQLIDentifier($this->table_prefix . "_" . $table_name) . " ";
340
341 foreach ($result as $index => $result_object) {
342
343 $data_in_order = array();
344 $format_in_order = array();
345 $columns_in_order = array();
346
347 foreach ($result_object as $column_name => $value) {
348
349 $data_in_order[] = $value;
350 $columns_in_order[] = BMP::escapeSQLIDentifier($column_name);
351
352 if (is_numeric($value)) {
353
354 if (is_float($value)) $format_in_order[] = '%f';
355 else $format_in_order[] = '%d';
356
357 } else $format_in_order[] = '%s';
358
359 }
360
361 if ($columns_schema_added === false) {
362
363 $query .= "(" . implode(', ', $columns_in_order) . ") VALUES ( \n";
364 $columns_schema_added = true;
365
366 } else {
367
368 $query = "), (\n";
369
370 }
371
372 $columns = sizeof($columns_in_order);
373 unset($columns_in_order);
374
375 $query .= "/* VALUES START */\n";
376 for ($i = 0; $i < $columns; ++$i) {
377
378 if ($format_in_order[$i] == '%f') {
379
380 $query .= floatval($data_in_order[$i]);
381
382 } elseif ($format_in_order[$i] == '%d') {
383
384 $query .= intval($data_in_order[$i]);
385
386 } else {
387
388 $query .= $this->wpdb->prepare("%s", $data_in_order[$i]);
389 $query = str_replace($this->percentage, '%', $query);
390
391 }
392
393 if ($i < ($columns - 1)) $query .= ",\n";
394 else $query .= "\n/* VALUES END */\n";
395
396 }
397
398 unset($data_in_order);
399 unset($format_in_order);
400 unset($columns_in_order);
401
402 fwrite($file, $query);
403
404 }
405
406 fwrite($file, ");\n/* QUERY END */\n\n");
407 fclose($file);
408 unset($file);
409
410 }
411
412 /**
413 * file_name - Replaces table name to file name friendly format
414 *
415 * @param {string} $table_name Table name
416 * @return {string} Friendly format for file
417 */
418 private function file_name($table_name) {
419
420 $friendly_name = preg_replace("/[^A-Za-z0-9_-]/", '', $table_name);
421 $friendly_name = trailingslashit($this->storage) . $friendly_name . '.sql';
422
423 return $friendly_name;
424
425 }
426
427 }
428