PluginProbe ʕ •ᴥ•ʔ
Backup Migration / 1.4.7
Backup Migration v1.4.7
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 / staging / local.php
backup-backup / includes / staging Last commit date
controller.php 1 year ago local.php 1 year ago tastewp.php 1 year ago
local.php
1018 lines
1 <?php
2
3 // Namespace
4 namespace BMI\Plugin\Staging;
5
6 // Use
7 use BMI\Plugin\Backup_Migration_Plugin as BMP;
8 use BMI\Plugin\Checker\BMI_Checker as Checker;
9 use BMI\Plugin\Database\BMI_Search_Replace_Engine as BMISearchReplace;
10
11 // Exit on direct access
12 if (!defined('ABSPATH')) exit;
13
14 // Require controller
15 require_once BMI_INCLUDES . '/staging/controller.php';
16
17 /**
18 * Subclass of main staging controller (local handler)
19 */
20 class BMI_StagingLocal extends BMI_Staging {
21
22 protected $data = [];
23 protected $step = 0;
24 private $filesList = null;
25 private $dirsList = null;
26 private $rootLength = 0;
27 private $excludedDirectories = [];
28
29 public function __construct($name, $initialize = false) {
30
31 parent::__construct(...func_get_args());
32 if ($initialize) $this->initialization();
33
34 }
35
36 public function continueProcess() {
37
38 if (isset($this->siteConfig['step'])) {
39 $this->step = intval($this->siteConfig['step']);
40 } else {
41 $this->step = null;
42 }
43
44 if (!is_numeric($this->step)) {
45
46 // End with error
47 $this->log(__('Step code was not provided for the request, prevents continuation of the process...'), 'ERROR');
48 $this->log('Step code was not provided for the request, prevents continuation of the process...', 'VERBOSE');
49 $this->log('#201', 'END-CODE');
50 return ['status' => 'error'];
51
52 }
53
54 // Default error
55 $translatedMainError = __('Something unexpected happened, we need to abort the process.', 'backup-backup');
56 $englishMainError = 'Something unexpected happened, we need to abort the process (#207).';
57
58 // Step controller
59 if ($this->step == 1) $this->prepareFilesAndDatabase();
60 else if ($this->step == 2) $this->duplicateDatabase();
61 else if ($this->step == 3) $this->searchReplace();
62 else if ($this->step == 4) $this->databaseFinishedCopyFiles();
63 else if ($this->step == 5) $this->setupWpConfigAndLoginScript();
64 else if ($this->step == 6) $this->performFinish();
65 else if (isset($this->step) && is_numeric($this->step)) $this->sendSuccess();
66 else $this->returnError($translatedMainError, $englishMainError);
67
68 }
69
70 private function getAllLiveTables() {
71
72 global $wpdb;
73 $tables = [];
74
75 $allTables = $wpdb->get_results('SHOW TABLES');
76
77 foreach ($allTables as $table) {
78 foreach ($table as $name) $tables[] = $name;
79 }
80
81 return $tables;
82
83 }
84
85 private function checkIfPrefixCanBeUsed($tables, $prefix) {
86
87 $sizeOfPrefix = strlen($prefix);
88
89 for ($i = 0; $i < sizeof($tables); ++$i) {
90 $name = $tables[$i];
91
92 if (substr($name, 0, $sizeOfPrefix) == $prefix) return false;
93 if (strpos($name, $prefix)) return false;
94 }
95
96 return $prefix;
97
98 }
99
100 private function generateUniquePrefix() {
101 return 'bmstg' . substr(time(), -4) . '_';
102 }
103
104 private function generateDatabasePrefix() {
105
106 $tables = $this->getAllLiveTables();
107 $prefix = $this->generateUniquePrefix();
108
109 $i = 0;
110
111 while ($this->checkIfPrefixCanBeUsed($tables, $prefix) === false && $i <= 3) {
112 sleep(1);
113 $prefix = $this->generateUniquePrefix();
114
115 $i++;
116 }
117
118 if ($i >= 3) return 'error';
119 return $prefix;
120
121 }
122
123 private function createDatabasePrefixAndInitDir() {
124
125 $path = trailingslashit(ABSPATH) . $this->name;
126
127 $this->log(__('Preparing constants and staging details', 'backup-backup'), 'STEP');
128
129 // Directory creation
130 $this->log(__('Creating root directory of new site', 'backup-backup'));
131 if (file_exists($path) && is_dir($path)) {
132 $translated = __('Seems like desired directory of staging site already exist, try with different staging site name.', 'backup-backup');
133 $english = 'Seems like desired directory of staging site already exist, try with different staging site name.';
134 $this->returnError($translated, $english);
135 }
136
137 @mkdir($path, 0755);
138 touch($path . DIRECTORY_SEPARATOR . '.bmi_staging');
139
140 $this->log(__('Path of new website:', 'backup-backup') . ' ' . $path, 'SUCCESS');
141
142 // Generation of database prefix
143 $this->log(__('Generating database prefix', 'backup-backup'));
144 $dbPrefix = $this->generateDatabasePrefix();
145 if ($dbPrefix == 'error') {
146 $translated = __('There was an error during database prefix generation, maybe all generated were already used, try again.', 'backup-backup');
147 $english = 'There was an error during database prefix generation, maybe all generated were already used, try again.';
148 $this->returnError($translated, $english);
149 }
150 $this->log(__('Prefix of new site will be:', 'backup-backup') . ' ' . $dbPrefix, 'SUCCESS');
151
152 $this->config[$this->name]['name'] = $this->name;
153 $this->config[$this->name]['prefix'] = $dbPrefix;
154
155 // Password for first log in
156 $this->log(__('Generating first log in password', 'backup-backup'));
157 $password = $this->getRandomPassword();
158 $this->log(__('Password created and saved.', 'backup-backup'), 'SUCCESS');
159
160 // Set initial configuration
161 $this->log(__('Initializing configuration of the website', 'backup-backup'), 'STEP');
162 $this->initialConfiguration($dbPrefix, $password);
163 $this->log(__('Configuration created', 'backup-backup'), 'SUCCESS');
164
165 }
166
167 private function initialConfiguration($dbPrefix, $password) {
168
169 global $table_prefix;
170
171 $ip = $this->getIpAddress();
172
173 // Basic for all
174 $this->siteConfig['name'] = $this->name; // Name of that staging site
175 $this->siteConfig['url'] = home_url($this->name); // URL to the website
176 $this->siteConfig['root_source'] = trailingslashit(ABSPATH); // ABSPATH of source website
177 $this->siteConfig['root_staging'] = untrailingslashit(ABSPATH) . DIRECTORY_SEPARATOR . $this->name; // ABSPATH of staging site
178 $this->siteConfig['db_prefix'] = $dbPrefix; // Database prefix of that site
179 $this->siteConfig['creation_date'] = time(); // Creation date and time of that staging site
180 $this->siteConfig['password'] = $password; // Password for password-less login
181 $this->siteConfig['creator_ip'] = $ip; // IP of user who created it
182 $this->siteConfig['login_ip'] = $ip; // IP for autologin script (limit password-less login to only that IP)
183 $this->siteConfig['login_user_id'] = get_current_user_id(); // User ID for password less authentication
184 $this->siteConfig['source_home_url'] = home_url(); // Homepage URL of source website
185 $this->siteConfig['source_site_url'] = site_url(); // Website (admin) URL of source website
186 $this->siteConfig['source_db_prefix'] = $table_prefix; // Database prefix of source website
187
188 // TasteWP
189 $this->siteConfig['communication_secret'] = 'local'; // Local if it's not TasteWP website // Secret code to get authless access to website
190 $this->siteConfig['expiration_time'] = 'never'; // Never if it's not TasteWP website // Expiration time of the website
191
192 }
193
194 private function getExcludedFilesAndDirectories($abspath = false) {
195
196 $excludedDirectories = [];
197
198 if ($abspath) {
199 $excludedDirectories = $this->getAllStagingSiteDirectories();
200 }
201
202 $excludedDirectories[] = '.';
203 $excludedDirectories[] = '..';
204 $excludedDirectories[] = 'wp-config.php';
205 $excludedDirectories[] = '.DS_Store';
206 $excludedDirectories[] = '.quarantine';
207 $excludedDirectories[] = '.git';
208 $excludedDirectories[] = '.tmb';
209 $excludedDirectories[] = 'node_modules';
210 $excludedDirectories[] = 'debug.log';
211
212 return $excludedDirectories;
213
214 }
215
216 private function getAbspathFiles($path) {
217
218 $excludedDirectories = $this->getExcludedFilesAndDirectories(true);
219 $topFiles = array_diff(scandir($path), $excludedDirectories);
220
221 return $topFiles;
222
223 }
224
225 private function processFiles($prefixPath, &$files) {
226
227 $dirs = [];
228
229 foreach ($files as $index => $file) {
230 $path = $prefixPath . $file;
231 if (strpos($path, BMI_BACKUPS_ROOT) !== false) continue;
232 if (is_link($path)) continue;
233 if (is_dir($path)) {
234 if (!(is_readable($path) && is_writable($path))) continue;
235 if (file_exists(trailingslashit($path) . '.bmi_staging')) continue;
236 $this->siteConfig['total_size'] += 4096;
237 $this->siteConfig['total_directories'] += 1;
238 fwrite($this->dirsList, $file . "\n");
239 $dirs[] = $path;
240 } else if (is_file($path)) {
241 if (!is_readable($path)) continue;
242 $this->siteConfig['total_size'] += filesize($path);
243 $this->siteConfig['total_files'] += 1;
244 fwrite($this->filesList, $file . "\n");
245 }
246 }
247
248 return $dirs;
249
250 }
251
252 private function processDirectoryRecursively($dir) {
253
254 if (!(is_readable($dir) && is_writable($dir))) return;
255
256 $files = array_diff(scandir($dir), $this->excludedDirectories);
257
258 foreach ($files as $key => $value) {
259 $path = $dir . DIRECTORY_SEPARATOR . $value;
260 if (strpos($path, BMI_BACKUPS_ROOT) !== false) continue;
261 if (is_link($path)) continue;
262 if (is_dir($path)) {
263 $this->siteConfig['total_size'] += 4096;
264 $this->siteConfig['total_directories'] += 1;
265 fwrite($this->dirsList, substr($path, $this->rootLength) . "\n");
266 if (!file_exists(trailingslashit($path) . '.bmi_staging')) {
267 $this->processDirectoryRecursively($path);
268 }
269 } else if (is_file($path)) {
270 if (!is_readable($path)) continue;
271 $this->siteConfig['total_size'] += filesize($path);
272 $this->siteConfig['total_files'] += 1;
273 fwrite($this->filesList, substr($path, $this->rootLength) . "\n");
274 }
275 }
276
277 }
278
279 private function processFilesRecursively($leftDirs) {
280
281 $this->rootLength = strlen(trailingslashit(ABSPATH));
282 $this->excludedDirectories = $this->getExcludedFilesAndDirectories();
283
284 for ($i = 0; $i < sizeof($leftDirs); ++$i) {
285 $this->processDirectoryRecursively($leftDirs[$i]);
286 }
287
288 }
289
290 private function getTablesForDuplication() {
291
292 global $wpdb, $table_prefix;
293
294 $currentPrefixes = $this->getAllStagingSitePrefixes();
295 $relatedTables = [];
296
297 $sql = "SELECT (DATA_LENGTH + INDEX_LENGTH) as `size`, TABLE_NAME AS `name` FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s;";
298 $sql = $wpdb->prepare($sql, array(DB_NAME));
299
300 $tables = $wpdb->get_results($sql);
301 foreach ($tables as $tableObject) {
302
303 $name = $tableObject->name;
304 $size = $tableObject->size;
305
306 if (substr($name, 0, strlen($table_prefix)) != $table_prefix) {
307 $this->log('Ommiting this table: ' . $name, 'verbose');
308 continue;
309 } else {
310 $this->log('Adding this table: ' . $name, 'verbose');
311 }
312
313 $tableOfStagingSite = false;
314 for ($i = 0; $i < sizeof($currentPrefixes); ++$i) {
315 $subPrefix = $currentPrefixes[$i];
316 if ($table_prefix != $subPrefix && substr($name, 0, strlen($subPrefix)) == $subPrefix) {
317 $tableOfStagingSite = true;
318 break;
319 }
320 }
321 if ($tableOfStagingSite) {
322 $this->log('Excluding this table as part of staging site: ' . $name, 'verbose');
323 continue;
324 }
325
326 $this->siteConfig['total_db_size'] += intval($size);
327 $relatedTables[$name] = $this->siteConfig['db_prefix'] . substr($name, strlen($table_prefix));
328
329 }
330
331 return $relatedTables;
332
333 }
334
335 private function updateUserRolesInOptions() {
336
337 global $wpdb;
338
339 // Update option name
340 $sql = "UPDATE %i SET `option_name` = %s WHERE `option_name` = %s;";
341 $newOptionTable = $this->siteConfig['db_prefix'] . 'options';
342 $newOptionName = $this->siteConfig['db_prefix'] . 'user_roles';
343 $oldOptionName = $this->siteConfig['source_db_prefix'] . 'user_roles';
344
345 $sql = $wpdb->prepare($sql, [$newOptionTable, $newOptionName, $oldOptionName]);
346 $wpdb->query($sql);
347
348 if ($wpdb->last_error !== '') {
349 $translated = __('There was an error during update of user roles:', 'backup-backup') . ' ' . $wpdb->last_error;
350 $english = 'There was an error during update of user roles:' . ' ' . $wpdb->last_error;
351 return $this->returnError($translated, $english);
352 }
353
354 $sql = "DELETE FROM %i WHERE `option_name` = 'BMI::STORAGE::LOCAL::PATH';";
355 $sql = $wpdb->prepare($sql, [$newOptionTable]);
356 $wpdb->query($sql);
357
358 if ($wpdb->last_error !== '') {
359 $translated = __('There was an error during BMI config hard removal:', 'backup-backup') . ' ' . $wpdb->last_error;
360 $english = 'There was an error during BMI config hard removal:' . ' ' . $wpdb->last_error;
361 return $this->returnError($translated, $english);
362 }
363
364 }
365
366 private function duplicateTableAlternative($source, $destination) {
367
368 global $wpdb;
369
370 // Remove failed table if created
371 $sql = "DROP TABLE IF EXISTS %i;";
372 $sql = $wpdb->prepare($sql, [$destination]);
373 $wpdb->query($sql);
374
375 if ($wpdb->last_error !== '') {
376 $translated = __('There was an error during previous destination table removal:', 'backup-backup') . ' ' . $wpdb->last_error;
377 $english = 'There was an error during previous destination table removal:' . ' ' . $wpdb->last_error;
378 return $this->returnError($translated, $english);
379 }
380
381 // Create new table
382 $sql = "CREATE TABLE %i LIKE %i;";
383 $sql = $wpdb->prepare($sql, [$destination, $source]);
384 $wpdb->query($sql);
385
386 if ($wpdb->last_error !== '') {
387 $translated = __('There was an error during database table creation:', 'backup-backup') . ' ' . $wpdb->last_error;
388 $english = 'There was an error during database table creation:' . ' ' . $wpdb->last_error;
389 return $this->returnError($translated, $english);
390 }
391
392 // Duplicate data
393 $sql = "INSERT INTO %i SELECT * from %i;";
394 $sql = $wpdb->prepare($sql, [$destination, $source]);
395 $wpdb->query($sql);
396
397 if ($wpdb->last_error !== '') {
398 $translated = __('There was an error during database table data duplication:', 'backup-backup') . ' ' . $wpdb->last_error;
399 $english = 'There was an error during database table data duplication:' . ' ' . $wpdb->last_error;
400 return $this->returnError($translated, $english);
401 }
402
403 }
404
405 private function duplicateTable($source, $destination) {
406
407 global $wpdb;
408
409 // Create new table
410 $sql = "SHOW CREATE TABLE %i;";
411 $sql = $wpdb->prepare($sql, [$source]);
412 $sourceCreate = $wpdb->get_row($sql);
413 $isTable = isset($sourceCreate->{'Create Table'});
414 if ($isTable) {
415 $sourceCreate->{'Create Table'} = str_ireplace('CREATE TABLE `' . $source, 'CREATE TABLE IF NOT EXISTS `' . $destination, $sourceCreate->{'Create Table'});
416 $wpdb->query($sourceCreate->{'Create Table'});
417 //Insert data
418 $sql = "INSERT INTO %i SELECT * from %i;";
419 $sql = $wpdb->prepare($sql, [$destination, $source]);
420 $wpdb->query($sql);
421 } else {
422 $sql = "CREATE TABLE %i AS SELECT * FROM %i;";
423 $sql = $wpdb->prepare($sql, [$destination, $source]);
424 $wpdb->query($sql);
425 }
426
427
428
429 if ($wpdb->last_error !== '') {
430 $this->duplicateTableAlternative($source, $destination);
431 }
432
433 if ($wpdb->last_error !== '') {
434 $translated = __('There was an error during database table creation:', 'backup-backup') . ' ' . $wpdb->last_error;
435 $english = 'There was an error during database table creation:' . ' ' . $wpdb->last_error;
436 return $this->returnError($translated, $english);
437 }
438
439 }
440
441 private function parseDomain($domain, $removeWWW = true) {
442
443 if (substr($domain, 0, 8) == 'https://') $domain = substr($domain, 8);
444 if (substr($domain, 0, 7) == 'http://') $domain = substr($domain, 7);
445 if ($removeWWW === true) {
446 if (substr($domain, 0, 4) == 'www.') $domain = substr($domain, 4);
447 }
448 $domain = untrailingslashit($domain);
449
450 return $domain;
451
452 }
453
454 private function performSearchReplace($table, $from, $to, $start = 0, $end = 0, $limitColumns = false) {
455
456 $replaceEngine = new BMISearchReplace([$table], $start, $end, true);
457
458 $stats = $replaceEngine->perform($from, $to, $limitColumns);
459
460 $this->siteConfig['totalTables'] += $stats['tables'];
461 $this->siteConfig['totalRows'] += $stats['rows'];
462 $this->siteConfig['totalChanges'] += $stats['change'];
463 $this->siteConfig['totalUpdates'] += $stats['updates'];
464
465 $this->siteConfig['currentTableTotalUpdates'] += $stats['updates'];
466 $this->siteConfig['currentSearchReplacePage'] = $stats['currentPage'];
467 $this->siteConfig['totalSearchReplacePages'] = $stats['totalPages'];
468
469 $replaceEngine = null;
470
471 }
472
473 public function requestDelete() {
474 return $this->abort(true);
475 }
476
477 // Step: 0 (initialization of the staging site, create entry)
478 private function initialization() {
479
480 $this->log('Step 0 - Initialization of the process', 'VERBOSE');
481
482 $this->log(__('Name of subsite:', 'backup-backup') . ' ' . $this->name);
483 $this->log(__('Expected URL of subsite:', 'backup-backup') . ' ' . home_url($this->name));
484
485 $this->printInitialLogs();
486
487 $this->createDatabasePrefixAndInitDir();
488 if ($this->wasError) return;
489
490 // Set progress to do something
491 $this->progress(4);
492
493 // Set next step to be 1
494 $this->setContinuation(1);
495
496 }
497
498 // Step: 1 (preparation of file list and database recipes)
499 private function prepareFilesAndDatabase() {
500
501 // Create list of all files and directories
502 if (!isset($this->siteConfig['batch']) || $this->siteConfig['batch'] == 1) {
503 $this->log(__('Scanning all files on your website, it may take a while...', 'backup-backup'), 'STEP');
504
505 $pathDirsListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_directories';
506 $pathFilesListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_files';
507
508 $this->siteConfig['total_size'] = 0;
509 $this->siteConfig['total_files'] = 0;
510 $this->siteConfig['total_directories'] = 0;
511
512 if (file_exists($pathDirsListFile)) @unlink($pathDirsListFile);
513 if (file_exists($pathFilesListFile)) @unlink($pathFilesListFile);
514
515 $this->filesList = fopen($pathFilesListFile, 'a');
516 $this->dirsList = fopen($pathDirsListFile, 'a');
517
518 $topLevelFiles = $this->getAbspathFiles(ABSPATH);
519 $leftDirs = $this->processFiles(trailingslashit(ABSPATH), $topLevelFiles);
520
521 $this->processFilesRecursively($leftDirs);
522
523 fclose($this->filesList);
524 fclose($this->dirsList);
525
526 $this->log(__('All files scanned and prepared for duplication...', 'backup-backup'), 'SUCCESS');
527 $this->progress(10);
528
529 $this->setContinuation(1, 2);
530 }
531
532 // Display details about scan and check database size
533 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 2) {
534 $this->log(__('Looking for database tables that should be duplicated and their sizes...', 'backup-backup'), 'STEP');
535
536 $this->siteConfig['total_db_size'] = 0;
537 $this->siteConfig['tables'] = $this->getTablesForDuplication();
538 $this->siteConfig['amountOfTables'] = sizeof($this->siteConfig['tables']);
539 $this->siteConfig['sumOfTotalSize'] = intval($this->siteConfig['total_db_size']) + intval($this->siteConfig['total_size']);
540
541 $this->log(__('Amount of files to duplicate:', 'backup-backup') . ' ' . $this->siteConfig['total_files']);
542 $this->log(__('Amount of directories to duplicate:', 'backup-backup') . ' ' . $this->siteConfig['total_directories']);
543 $this->log(__('Amount of tables to duplicate:', 'backup-backup') . ' ' . $this->siteConfig['amountOfTables']);
544 $this->log(__('Size of database to duplicate:', 'backup-backup') . ' ' . BMP::humanSize(intval($this->siteConfig['total_db_size'])));
545 $this->log(__('Size of files to duplicate:', 'backup-backup') . ' ' . BMP::humanSize(intval($this->siteConfig['total_size'])));
546 $this->log(__('Total duplication size:', 'backup-backup') . ' ' . BMP::humanSize($this->siteConfig['sumOfTotalSize']));
547 $this->log(__('Tables prepared for duplication...', 'backup-backup'), 'SUCCESS');
548
549 $this->log(__('Checking if there is enough space for the staging site...', 'backup-backup'), 'STEP');
550 $this->log(__('Space checking may take a while', 'backup-backup'));
551
552 $this->progress(15);
553
554 $this->setContinuation(1, 3);
555 }
556
557 // Check if there is enough space for duplication
558 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 3) {
559 require_once BMI_INCLUDES . '/check/checker.php';
560 $checker = new Checker($this->logger);
561
562 $bytes = intval(intval($this->siteConfig['sumOfTotalSize']) * 1.1);
563 $this->log(str_replace('%s2', BMP::humanSize($bytes), str_replace('%s1', $bytes, __('Checking in total: %s1 bytes (%s2)', 'backup-backup'))));
564
565 if (!$checker->check_free_space($bytes, true)) {
566
567 $translated = __('There is not enough space on your server in order to create staging site.', 'backup-backup');
568 $english = 'There is not enough space on your server in order to create staging site.';
569 return $this->returnError($translated, $english);
570
571 } else {
572
573 $this->log(__("Confirmed, there is more than enough space, checked: ", 'backup-backup') . ($bytes) . __(" bytes", 'backup-backup'), 'SUCCESS');
574
575 }
576
577 $this->progress(20);
578
579 // Set next batch to start step 2
580 $this->log('Setting new step for next request to 2 @ batch 1', 'VERBOSE');
581 $this->setContinuation(2);
582
583 }
584
585 }
586
587 // Step: 2 (duplicate database)
588 private function duplicateDatabase() {
589
590 if (!isset($this->siteConfig['finishedTables'])) {
591 $this->siteConfig['finishedTables'] = [];
592 }
593
594 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 1) {
595 $this->log(__('Duplicating database tables...', 'backup-backup'), 'STEP');
596 }
597
598 $startTime = time();
599 foreach ($this->siteConfig['tables'] as $source_table => $destination_table) {
600
601 $this->duplicateTable($source_table, $destination_table);
602 $this->log(str_replace('%s1', $source_table, str_replace('%s2', $destination_table, __('Table %s1 cloned as %s2', 'backup-backup'))));
603
604 $this->siteConfig['finishedTables'][] = $destination_table;
605 unset($this->siteConfig['tables'][$source_table]);
606
607 $processPercentage = intval((sizeof($this->siteConfig['finishedTables']) / $this->siteConfig['amountOfTables']) * 20);
608 $this->progress(20 + $processPercentage);
609
610 if ((time() - $startTime) >= 4) {
611 $this->setContinuation(2, (intval($this->siteConfig['batch']) + 1));
612 break;
613 }
614
615 }
616
617 if (sizeof($this->siteConfig['tables']) == 0) {
618 unset($this->siteConfig['tables']);
619 }
620
621 if (!isset($this->siteConfig['tables'])) {
622 $this->log(__('All tables were successfully duplicated', 'backup-backup'), 'SUCCESS');
623 $this->setContinuation(3);
624 }
625
626 }
627
628 // Step: 3 (search & replace of new tables)
629 private function searchReplace() {
630
631 require_once BMI_INCLUDES . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'search-replace.php';
632
633 // Domain
634 $sourceURL = $this->parseDomain($this->siteConfig['source_home_url']);
635 $destinationURL = $this->parseDomain($this->siteConfig['url']);
636
637 // Paths
638 $sourceABSPATH = untrailingslashit($this->siteConfig['root_source']);
639 $destinationABSPATH = untrailingslashit($this->siteConfig['root_staging']);
640
641 // Items to be replaced
642 $replaces = [
643 ['from' => $sourceABSPATH, 'to' => $destinationABSPATH],
644 ['from' => $sourceURL, 'to' => $destinationURL],
645 ['from' => $this->siteConfig['source_db_prefix'], 'to' => $this->siteConfig['db_prefix']]
646 ];
647
648 $variants = [
649 __("Batch for path adjustment (%s/%s) updated: %s fields.", 'backup-backup'),
650 __("Batch for domain adjustments (%s/%s) updated: %s fields.", 'backup-backup'),
651 __("Batch for prefix key adjustments (%s/%s) updated: %s fields.", 'backup-backup')
652 ];
653
654 $variantsEmpty = [
655 __("Path replacements are not required for table: %s", 'backup-backup'),
656 __("Domain replacements are not required for table: %s", 'backup-backup'),
657 __("Prefix key replacements are not required for table: %s", 'backup-backup')
658 ];
659
660 // Table for replacement
661 if (isset($this->siteConfig['finishedTables']) && sizeof($this->siteConfig['finishedTables']) > 0) {
662
663 // Get first table from duplicated tables
664 $table = $this->siteConfig['finishedTables'][0];
665
666 } else {
667
668 // All tables processed, search replace finished
669 unset($this->siteConfig['finishedTables']);
670 $this->setContinuation(4);
671
672 }
673
674 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 1) {
675 $this->log(__('Performing database search replace on duplicated tables...', 'backup-backup'), 'STEP');
676
677 $pagesize = '?';
678 if (defined('BMI_MAX_SEARCH_REPLACE_PAGE')) $pagesize = BMI_MAX_SEARCH_REPLACE_PAGE;
679 $this->log(__('Page size for that process: ', 'backup-backup') . $pagesize, 'INFO');
680
681 $this->siteConfig['totalTables'] = 0;
682 $this->siteConfig['totalRows'] = 0;
683 $this->siteConfig['totalChanges'] = 0;
684 $this->siteConfig['totalUpdates'] = 0;
685
686 $this->siteConfig['currentSearchReplaceItem'] = 0;
687 $this->siteConfig['currentTableTotalUpdates'] = 0;
688 $this->siteConfig['currentSearchReplacePage'] = 0;
689 $this->siteConfig['totalSearchReplacePages'] = 0;
690
691 $this->setContinuation(3, 2);
692 }
693
694 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 2) {
695
696 // Give details of current process
697 if ($this->siteConfig['currentSearchReplaceItem'] == 0 && $this->siteConfig['currentSearchReplacePage'] == 0) {
698 $this->log(sprintf(__('Performing search replace for table: %s', 'backup-backup'), $table), 'STEP');
699 }
700
701 // Control time of the process
702 $startTime = time();
703 $tableCompleted = false;
704 $maxExecutionTime = 5;
705
706 for ($i = $this->siteConfig['currentSearchReplaceItem']; $i < sizeof($replaces); ++$i) {
707
708 $this->siteConfig['currentSearchReplaceItem'] = $i;
709 if ((time() - $startTime) >= $maxExecutionTime) break;
710 $tableCompleted = false;
711
712 $from = $replaces[$i]['from'];
713 $to = $replaces[$i]['to'];
714
715 while ($tableCompleted === false && (time() - $startTime) < $maxExecutionTime) {
716
717 // Progress
718 $start = $this->siteConfig['currentSearchReplacePage'];
719 $end = $this->siteConfig['totalSearchReplacePages'];
720
721 // Path replace for current table
722 if ($i == 2) {
723 if (strpos($table, 'usermeta') !== false) {
724 $this->performSearchReplace($table, $from, $to, $start, $end, ['meta_key']);
725 }
726 } else {
727 $this->performSearchReplace($table, $from, $to, $start, $end);
728 }
729
730 // Handle batch logs
731 if ($this->siteConfig['currentTableTotalUpdates'] == 0 && $this->siteConfig['totalSearchReplacePages'] == 0) {
732 $this->log(sprintf($variantsEmpty[$i], $table), 'INFO');
733 } else {
734 $st = $this->siteConfig['currentSearchReplacePage'];
735 $en = $this->siteConfig['totalSearchReplacePages'];
736 $this->log(sprintf($variants[$i], $st, $en, $this->siteConfig['currentTableTotalUpdates']));
737 $this->siteConfig['currentTableTotalUpdates'] = 0;
738 }
739
740 // Done of that table
741 if ($this->siteConfig['currentSearchReplacePage'] >= $this->siteConfig['totalSearchReplacePages']) {
742 if ($i >= sizeof($replaces) - 1) {
743 unset($this->siteConfig['finishedTables'][0]);
744 $this->siteConfig['finishedTables'] = array_values($this->siteConfig['finishedTables']);
745 $this->log(sprintf(__('Search replace for table %s finished', 'backup-backup'), $table), 'SUCCESS');
746 $this->siteConfig['currentSearchReplaceItem'] = 0;
747
748 $processPercentage = intval((($this->siteConfig['amountOfTables'] - sizeof($this->siteConfig['finishedTables'])) / $this->siteConfig['amountOfTables']) * 20);
749 $this->progress(40 + $processPercentage);
750 }
751
752 $this->siteConfig['currentTableTotalUpdates'] = 0;
753 $this->siteConfig['currentSearchReplacePage'] = 0;
754 $this->siteConfig['totalSearchReplacePages'] = 0;
755 $tableCompleted = true;
756 }
757
758 }
759
760 if (!$tableCompleted) break;
761
762 }
763
764 if ($tableCompleted && sizeof($this->siteConfig['finishedTables']) == 0) $this->setContinuation(3, 3); // If path replacement finished
765 else $this->setContinuation(3, 2); // If path raplecement didnt finish, continue next batch (time exceed 5 seconds)
766
767 }
768
769 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 3) $this->setContinuation(4);
770
771 }
772
773 // Step: 4 (summary of S&R and file duplication)
774 private function databaseFinishedCopyFiles() {
775
776 $totalBatchExecution = 5; // 5 seconds
777 $milestoneUpdate = 500; // per 500 files/directories
778 $batchLimit = $milestoneUpdate + 1; // Limit of files/directories per batch
779
780 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 1) {
781
782 // Remove unused variables
783 unset($this->siteConfig['currentSearchReplaceItem']);
784 unset($this->siteConfig['currentTableTotalUpdates']);
785 unset($this->siteConfig['currentSearchReplacePage']);
786 unset($this->siteConfig['totalSearchReplacePages']);
787
788 // Display summary of search replace
789 $this->log(__('Displaying summary of search replace process', 'backup-backup'), 'STEP');
790 $this->log(sprintf(__('Search replace processed %s tables in total', 'backup-backup'), $this->siteConfig['totalTables']));
791 $this->log(sprintf(__('In total it processed %s rows', 'backup-backup'), $this->siteConfig['totalRows']));
792 $this->log(sprintf(__('After all it changed only %s columns', 'backup-backup'), $this->siteConfig['totalChanges']));
793 $this->log(sprintf(__('Which results in %s changes in total of all cells', 'backup-backup'), $this->siteConfig['totalUpdates']));
794
795 // Remove unused variables
796 unset($this->siteConfig['totalTables']);
797 unset($this->siteConfig['totalRows']);
798 unset($this->siteConfig['totalChanges']);
799 unset($this->siteConfig['totalUpdates']);
800
801 // Update user roles in options table
802 $this->updateUserRolesInOptions();
803
804 $this->log(__('Search replace of all tables finished successfully', 'backup-backup'), 'SUCCESS');
805
806 // Go to new process without new batch request
807 $this->siteConfig['batch'] = 2;
808
809 }
810
811 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 2) {
812
813 if (!isset($this->siteConfig['dirSeek']) || $this->siteConfig['dirSeek'] == 0) {
814 $this->log(__('Duplication of directories to staging site', 'backup-backup'), 'STEP');
815 $this->siteConfig['dirSeek'] = 0;
816 }
817
818 $pathDirsListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_directories';
819
820 $file = new \SplFileObject($pathDirsListFile);
821 $file->seek($file->getSize());
822
823 $startTime = time();
824 $currentSeekedElements = 0;
825 $totalLines = $file->key() + 1;
826 $stagingRoot = trailingslashit($this->siteConfig['root_staging']);
827 $file->seek($this->siteConfig['dirSeek']);
828
829 while (!$file->eof()) {
830
831 if ((time() - $startTime) > $totalBatchExecution || $currentSeekedElements > $batchLimit) break;
832
833 $file->seek($this->siteConfig['dirSeek']);
834 $path = $stagingRoot . trim($file->current());
835
836 if (!(file_exists($path) && is_dir($path))) @mkdir($path);
837 if ($this->siteConfig['dirSeek'] % $milestoneUpdate === 0 && $this->siteConfig['dirSeek'] != 0) {
838 $processPercentageTotal = number_format(($this->siteConfig['dirSeek'] / $this->siteConfig['total_directories']) * 100, 2);
839 $this->log(sprintf(
840 __('Directory creation milestone: %s/%s (%s)', 'backup-backup'),
841 $this->siteConfig['dirSeek'],
842 $this->siteConfig['total_directories'],
843 $processPercentageTotal . '%'
844 ));
845 $processPercentage = intval(($this->siteConfig['dirSeek'] / $this->siteConfig['total_directories']) * 5);
846 $this->progress(60 + $processPercentage);
847 }
848
849 $this->siteConfig['dirSeek']++;
850 $currentSeekedElements++;
851
852 }
853
854 if ($this->siteConfig['dirSeek'] >= $this->siteConfig['total_directories']) {
855 unlink($pathDirsListFile);
856 unset($this->siteConfig['dirSeek']);
857 $this->log(sprintf(
858 __('Directory creation milestone: %s/%s (%s)', 'backup-backup'),
859 $this->siteConfig['total_directories'],
860 $this->siteConfig['total_directories'],
861 '100%'
862 ));
863 $this->progress(65);
864 $this->setContinuation(4, 3);
865 } else {
866 $this->setContinuation(4, 2);
867 }
868
869 }
870
871 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 3) {
872
873 if (!isset($this->siteConfig['fileSeek']) || $this->siteConfig['fileSeek'] == 0) {
874 $this->log(__('Duplication of files to staging site', 'backup-backup'), 'STEP');
875 $this->log(__('Duplication of files make take a while', 'backup-backup'));
876 $this->log(__('Disclaimer: ZIP Archives are not getting cloned on staging sites', 'backup-backup'), 'WARN');
877 $this->siteConfig['fileSeek'] = 0;
878 }
879
880 $pathFilesListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_files';
881
882 $file = new \SplFileObject($pathFilesListFile);
883 $file->seek($file->getSize());
884
885 $startTime = time();
886 $currentSeekedElements = 0;
887 $totalLines = $file->key() + 1;
888 $sourceRoot = trailingslashit($this->siteConfig['root_source']);
889 $stagingRoot = trailingslashit($this->siteConfig['root_staging']);
890 $file->seek($this->siteConfig['fileSeek']);
891 $disallowedExtensions = ['zip', 'tar', 'gz', 'tmp', 'rar', '7z'];
892
893 while (!$file->eof()) {
894
895 if ((time() - $startTime) > $totalBatchExecution || $currentSeekedElements > $batchLimit) break;
896
897 $file->seek($this->siteConfig['fileSeek']);
898 $path = trim($file->current());
899
900 if (file_exists($sourceRoot . $path) && !file_exists($stagingRoot . $path)) {
901 $arrayWithExtension = explode('.', $path);
902 $ext = strtolower(array_pop($arrayWithExtension));
903 if (!in_array($ext, $disallowedExtensions) && strpos($path, 'backup-migration-config.php') === false) {
904 @copy($sourceRoot . $path, $stagingRoot . $path);
905 }
906 }
907
908 if ($this->siteConfig['fileSeek'] % $milestoneUpdate === 0 && $this->siteConfig['fileSeek'] != 0) {
909 $processPercentageTotal = number_format(($this->siteConfig['fileSeek'] / $this->siteConfig['total_files']) * 100, 2);
910 $this->log(sprintf(
911 __('File duplication milestone: %s/%s (%s)', 'backup-backup'),
912 $this->siteConfig['fileSeek'],
913 $this->siteConfig['total_files'],
914 $processPercentageTotal . '%'
915 ));
916 $processPercentage = intval(($this->siteConfig['fileSeek'] / $this->siteConfig['total_files']) * 25);
917 $this->progress(65 + $processPercentage);
918 }
919
920 $this->siteConfig['fileSeek']++;
921 $currentSeekedElements++;
922
923 }
924
925 if ($this->siteConfig['fileSeek'] >= $this->siteConfig['total_files']) {
926 unlink($pathFilesListFile);
927 unset($this->siteConfig['fileSeek']);
928 $this->log(sprintf(
929 __('File duplication milestone: %s/%s (%s)', 'backup-backup'),
930 $this->siteConfig['total_files'],
931 $this->siteConfig['total_files'],
932 '100%'
933 ));
934 $this->progress(90);
935 $this->setContinuation(5);
936 } else {
937 $this->setContinuation(4, 3);
938 }
939
940 }
941
942 }
943
944 // Step: 5 (setup passwordless login script and paste adjusted wp-config)
945 private function setupWpConfigAndLoginScript() {
946
947 $this->copyOverPasswordLessScript();
948
949 $this->log(__('Inserting wp-config.php file for staging site', 'backup-backup'), 'STEP');
950
951 $sourceWPConfig = trailingslashit($this->siteConfig['root_source']) . 'wp-config.php';
952 $destinationWPConfig = trailingslashit($this->siteConfig['root_staging']) . 'wp-config.php';
953
954 $previousPrefix = $this->siteConfig['source_db_prefix'];
955 $destinationPrefix = $this->siteConfig['db_prefix'];
956
957 $previousRoot = untrailingslashit($this->siteConfig['root_source']);
958 $destinationRoot = untrailingslashit($this->siteConfig['root_staging']);
959
960 $sourceURL = $this->parseDomain($this->siteConfig['source_home_url']);
961 $destinationURL = $this->parseDomain($this->siteConfig['url']);
962
963 $wpconfig = file_get_contents($sourceWPConfig);
964
965 // Table Prefix
966 $this->log(__('Replacing table prefix in wp-config.php', 'backup-backup'));
967 if (strpos($wpconfig, '"' . $previousPrefix . '";') !== false) {
968 $wpconfig = str_replace('"' . $previousPrefix . '";', '"' . $destinationPrefix . '";', $wpconfig);
969 } elseif (strpos($wpconfig, "'" . $previousPrefix . "';") !== false) {
970 $wpconfig = str_replace("'" . $previousPrefix . "';", "'" . $destinationPrefix . "';", $wpconfig);
971 }
972
973 // Paths e.g. for wp_debug_log
974 $this->log(__('Adjusting paths in wp-config.php', 'backup-backup'));
975 if (strpos($wpconfig, '"' . $previousRoot . '";') !== false) {
976 $wpconfig = str_replace('"' . $previousRoot . '";', '"' . $destinationRoot . '";', $wpconfig);
977 } elseif (strpos($wpconfig, "'" . $previousRoot . "';") !== false) {
978 $wpconfig = str_replace("'" . $previousRoot . "';", "'" . $destinationRoot . "';", $wpconfig);
979 }
980
981 // Paths e.g. for wp_home & wp_siteurl
982 $this->log(__('Adjusting domains in wp-config.php', 'backup-backup'));
983 $wpconfig = explode("\n", $wpconfig);
984 for ($i = 0; $i < sizeof($wpconfig); ++$i) {
985
986 $line = $wpconfig[$i];
987
988 if (strpos($line, 'WP_SITEURL') !== false || strpos($line, 'WP_HOME') !== false) {
989 $wpconfig[$i] = str_replace($sourceURL, $destinationURL, $line);
990 }
991
992 }
993
994 $wpconfig = implode("\n", $wpconfig);
995
996 file_put_contents($destinationWPConfig, $wpconfig);
997 $this->log(__('File adjustments for wp-config.php finished successfully', 'backup-backup'), 'SUCCESS');
998
999 $this->setContinuation(6);
1000
1001 }
1002
1003 // Step: 6 (cleanup and misc)
1004 private function performFinish() {
1005
1006 $this->cleanup();
1007 $this->setContinuation(7);
1008
1009 }
1010
1011 public function __destruct() {
1012
1013 parent::__destruct();
1014
1015 }
1016
1017 }
1018