PluginProbe ʕ •ᴥ•ʔ
Backup Migration / 1.4.5
Backup Migration v1.4.5
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 2 years ago local.php 2 years ago tastewp.php 2 years ago
local.php
1003 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 = "CREATE TABLE %i AS SELECT * FROM %i;";
411 $sql = $wpdb->prepare($sql, [$destination, $source]);
412 $wpdb->query($sql);
413
414 if ($wpdb->last_error !== '') {
415 $this->duplicateTableAlternative($source, $destination);
416 }
417
418 if ($wpdb->last_error !== '') {
419 $translated = __('There was an error during database table creation:', 'backup-backup') . ' ' . $wpdb->last_error;
420 $english = 'There was an error during database table creation:' . ' ' . $wpdb->last_error;
421 return $this->returnError($translated, $english);
422 }
423
424 }
425
426 private function parseDomain($domain, $removeWWW = true) {
427
428 if (substr($domain, 0, 8) == 'https://') $domain = substr($domain, 8);
429 if (substr($domain, 0, 7) == 'http://') $domain = substr($domain, 7);
430 if ($removeWWW === true) {
431 if (substr($domain, 0, 4) == 'www.') $domain = substr($domain, 4);
432 }
433 $domain = untrailingslashit($domain);
434
435 return $domain;
436
437 }
438
439 private function performSearchReplace($table, $from, $to, $start = 0, $end = 0, $limitColumns = false) {
440
441 $replaceEngine = new BMISearchReplace([$table], $start, $end, true);
442
443 $stats = $replaceEngine->perform($from, $to, $limitColumns);
444
445 $this->siteConfig['totalTables'] += $stats['tables'];
446 $this->siteConfig['totalRows'] += $stats['rows'];
447 $this->siteConfig['totalChanges'] += $stats['change'];
448 $this->siteConfig['totalUpdates'] += $stats['updates'];
449
450 $this->siteConfig['currentTableTotalUpdates'] += $stats['updates'];
451 $this->siteConfig['currentSearchReplacePage'] = $stats['currentPage'];
452 $this->siteConfig['totalSearchReplacePages'] = $stats['totalPages'];
453
454 $replaceEngine = null;
455
456 }
457
458 public function requestDelete() {
459 return $this->abort(true);
460 }
461
462 // Step: 0 (initialization of the staging site, create entry)
463 private function initialization() {
464
465 $this->log('Step 0 - Initialization of the process', 'VERBOSE');
466
467 $this->log(__('Name of subsite:', 'backup-backup') . ' ' . $this->name);
468 $this->log(__('Expected URL of subsite:', 'backup-backup') . ' ' . home_url($this->name));
469
470 $this->printInitialLogs();
471
472 $this->createDatabasePrefixAndInitDir();
473 if ($this->wasError) return;
474
475 // Set progress to do something
476 $this->progress(4);
477
478 // Set next step to be 1
479 $this->setContinuation(1);
480
481 }
482
483 // Step: 1 (preparation of file list and database recipes)
484 private function prepareFilesAndDatabase() {
485
486 // Create list of all files and directories
487 if (!isset($this->siteConfig['batch']) || $this->siteConfig['batch'] == 1) {
488 $this->log(__('Scanning all files on your website, it may take a while...', 'backup-backup'), 'STEP');
489
490 $pathDirsListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_directories';
491 $pathFilesListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_files';
492
493 $this->siteConfig['total_size'] = 0;
494 $this->siteConfig['total_files'] = 0;
495 $this->siteConfig['total_directories'] = 0;
496
497 if (file_exists($pathDirsListFile)) @unlink($pathDirsListFile);
498 if (file_exists($pathFilesListFile)) @unlink($pathFilesListFile);
499
500 $this->filesList = fopen($pathFilesListFile, 'a');
501 $this->dirsList = fopen($pathDirsListFile, 'a');
502
503 $topLevelFiles = $this->getAbspathFiles(ABSPATH);
504 $leftDirs = $this->processFiles(trailingslashit(ABSPATH), $topLevelFiles);
505
506 $this->processFilesRecursively($leftDirs);
507
508 fclose($this->filesList);
509 fclose($this->dirsList);
510
511 $this->log(__('All files scanned and prepared for duplication...', 'backup-backup'), 'SUCCESS');
512 $this->progress(10);
513
514 $this->setContinuation(1, 2);
515 }
516
517 // Display details about scan and check database size
518 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 2) {
519 $this->log(__('Looking for database tables that should be duplicated and their sizes...', 'backup-backup'), 'STEP');
520
521 $this->siteConfig['total_db_size'] = 0;
522 $this->siteConfig['tables'] = $this->getTablesForDuplication();
523 $this->siteConfig['amountOfTables'] = sizeof($this->siteConfig['tables']);
524 $this->siteConfig['sumOfTotalSize'] = intval($this->siteConfig['total_db_size']) + intval($this->siteConfig['total_size']);
525
526 $this->log(__('Amount of files to duplicate:', 'backup-backup') . ' ' . $this->siteConfig['total_files']);
527 $this->log(__('Amount of directories to duplicate:', 'backup-backup') . ' ' . $this->siteConfig['total_directories']);
528 $this->log(__('Amount of tables to duplicate:', 'backup-backup') . ' ' . $this->siteConfig['amountOfTables']);
529 $this->log(__('Size of database to duplicate:', 'backup-backup') . ' ' . BMP::humanSize(intval($this->siteConfig['total_db_size'])));
530 $this->log(__('Size of files to duplicate:', 'backup-backup') . ' ' . BMP::humanSize(intval($this->siteConfig['total_size'])));
531 $this->log(__('Total duplication size:', 'backup-backup') . ' ' . BMP::humanSize($this->siteConfig['sumOfTotalSize']));
532 $this->log(__('Tables prepared for duplication...', 'backup-backup'), 'SUCCESS');
533
534 $this->log(__('Checking if there is enough space for the staging site...', 'backup-backup'), 'STEP');
535 $this->log(__('Space checking may take a while', 'backup-backup'));
536
537 $this->progress(15);
538
539 $this->setContinuation(1, 3);
540 }
541
542 // Check if there is enough space for duplication
543 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 3) {
544 require_once BMI_INCLUDES . '/check/checker.php';
545 $checker = new Checker($this->logger);
546
547 $bytes = intval(intval($this->siteConfig['sumOfTotalSize']) * 1.1);
548 $this->log(str_replace('%s2', BMP::humanSize($bytes), str_replace('%s1', $bytes, __('Checking in total: %s1 bytes (%s2)', 'backup-backup'))));
549
550 if (!$checker->check_free_space($bytes, true)) {
551
552 $translated = __('There is not enough space on your server in order to create staging site.', 'backup-backup');
553 $english = 'There is not enough space on your server in order to create staging site.';
554 return $this->returnError($translated, $english);
555
556 } else {
557
558 $this->log(__("Confirmed, there is more than enough space, checked: ", 'backup-backup') . ($bytes) . __(" bytes", 'backup-backup'), 'SUCCESS');
559
560 }
561
562 $this->progress(20);
563
564 // Set next batch to start step 2
565 $this->log('Setting new step for next request to 2 @ batch 1', 'VERBOSE');
566 $this->setContinuation(2);
567
568 }
569
570 }
571
572 // Step: 2 (duplicate database)
573 private function duplicateDatabase() {
574
575 if (!isset($this->siteConfig['finishedTables'])) {
576 $this->siteConfig['finishedTables'] = [];
577 }
578
579 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 1) {
580 $this->log(__('Duplicating database tables...', 'backup-backup'), 'STEP');
581 }
582
583 $startTime = time();
584 foreach ($this->siteConfig['tables'] as $source_table => $destination_table) {
585
586 $this->duplicateTable($source_table, $destination_table);
587 $this->log(str_replace('%s1', $source_table, str_replace('%s2', $destination_table, __('Table %s1 cloned as %s2', 'backup-backup'))));
588
589 $this->siteConfig['finishedTables'][] = $destination_table;
590 unset($this->siteConfig['tables'][$source_table]);
591
592 $processPercentage = intval((sizeof($this->siteConfig['finishedTables']) / $this->siteConfig['amountOfTables']) * 20);
593 $this->progress(20 + $processPercentage);
594
595 if ((time() - $startTime) >= 4) {
596 $this->setContinuation(2, (intval($this->siteConfig['batch']) + 1));
597 break;
598 }
599
600 }
601
602 if (sizeof($this->siteConfig['tables']) == 0) {
603 unset($this->siteConfig['tables']);
604 }
605
606 if (!isset($this->siteConfig['tables'])) {
607 $this->log(__('All tables were successfully duplicated', 'backup-backup'), 'SUCCESS');
608 $this->setContinuation(3);
609 }
610
611 }
612
613 // Step: 3 (search & replace of new tables)
614 private function searchReplace() {
615
616 require_once BMI_INCLUDES . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'search-replace.php';
617
618 // Domain
619 $sourceURL = $this->parseDomain($this->siteConfig['source_home_url']);
620 $destinationURL = $this->parseDomain($this->siteConfig['url']);
621
622 // Paths
623 $sourceABSPATH = untrailingslashit($this->siteConfig['root_source']);
624 $destinationABSPATH = untrailingslashit($this->siteConfig['root_staging']);
625
626 // Items to be replaced
627 $replaces = [
628 ['from' => $sourceABSPATH, 'to' => $destinationABSPATH],
629 ['from' => $sourceURL, 'to' => $destinationURL],
630 ['from' => $this->siteConfig['source_db_prefix'], 'to' => $this->siteConfig['db_prefix']]
631 ];
632
633 $variants = [
634 __("Batch for path adjustment (%s/%s) updated: %s fields.", 'backup-backup'),
635 __("Batch for domain adjustments (%s/%s) updated: %s fields.", 'backup-backup'),
636 __("Batch for prefix key adjustments (%s/%s) updated: %s fields.", 'backup-backup')
637 ];
638
639 $variantsEmpty = [
640 __("Path replacements are not required for table: %s", 'backup-backup'),
641 __("Domain replacements are not required for table: %s", 'backup-backup'),
642 __("Prefix key replacements are not required for table: %s", 'backup-backup')
643 ];
644
645 // Table for replacement
646 if (isset($this->siteConfig['finishedTables']) && sizeof($this->siteConfig['finishedTables']) > 0) {
647
648 // Get first table from duplicated tables
649 $table = $this->siteConfig['finishedTables'][0];
650
651 } else {
652
653 // All tables processed, search replace finished
654 unset($this->siteConfig['finishedTables']);
655 $this->setContinuation(4);
656
657 }
658
659 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 1) {
660 $this->log(__('Performing database search replace on duplicated tables...', 'backup-backup'), 'STEP');
661
662 $pagesize = '?';
663 if (defined('BMI_MAX_SEARCH_REPLACE_PAGE')) $pagesize = BMI_MAX_SEARCH_REPLACE_PAGE;
664 $this->log(__('Page size for that process: ', 'backup-backup') . $pagesize, 'INFO');
665
666 $this->siteConfig['totalTables'] = 0;
667 $this->siteConfig['totalRows'] = 0;
668 $this->siteConfig['totalChanges'] = 0;
669 $this->siteConfig['totalUpdates'] = 0;
670
671 $this->siteConfig['currentSearchReplaceItem'] = 0;
672 $this->siteConfig['currentTableTotalUpdates'] = 0;
673 $this->siteConfig['currentSearchReplacePage'] = 0;
674 $this->siteConfig['totalSearchReplacePages'] = 0;
675
676 $this->setContinuation(3, 2);
677 }
678
679 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 2) {
680
681 // Give details of current process
682 if ($this->siteConfig['currentSearchReplaceItem'] == 0 && $this->siteConfig['currentSearchReplacePage'] == 0) {
683 $this->log(sprintf(__('Performing search replace for table: %s', 'backup-backup'), $table), 'STEP');
684 }
685
686 // Control time of the process
687 $startTime = time();
688 $tableCompleted = false;
689 $maxExecutionTime = 5;
690
691 for ($i = $this->siteConfig['currentSearchReplaceItem']; $i < sizeof($replaces); ++$i) {
692
693 $this->siteConfig['currentSearchReplaceItem'] = $i;
694 if ((time() - $startTime) >= $maxExecutionTime) break;
695 $tableCompleted = false;
696
697 $from = $replaces[$i]['from'];
698 $to = $replaces[$i]['to'];
699
700 while ($tableCompleted === false && (time() - $startTime) < $maxExecutionTime) {
701
702 // Progress
703 $start = $this->siteConfig['currentSearchReplacePage'];
704 $end = $this->siteConfig['totalSearchReplacePages'];
705
706 // Path replace for current table
707 if ($i == 2) {
708 if (strpos($table, 'usermeta') !== false) {
709 $this->performSearchReplace($table, $from, $to, $start, $end, ['meta_key']);
710 }
711 } else {
712 $this->performSearchReplace($table, $from, $to, $start, $end);
713 }
714
715 // Handle batch logs
716 if ($this->siteConfig['currentTableTotalUpdates'] == 0 && $this->siteConfig['totalSearchReplacePages'] == 0) {
717 $this->log(sprintf($variantsEmpty[$i], $table), 'INFO');
718 } else {
719 $st = $this->siteConfig['currentSearchReplacePage'];
720 $en = $this->siteConfig['totalSearchReplacePages'];
721 $this->log(sprintf($variants[$i], $st, $en, $this->siteConfig['currentTableTotalUpdates']));
722 $this->siteConfig['currentTableTotalUpdates'] = 0;
723 }
724
725 // Done of that table
726 if ($this->siteConfig['currentSearchReplacePage'] >= $this->siteConfig['totalSearchReplacePages']) {
727 if ($i >= sizeof($replaces) - 1) {
728 unset($this->siteConfig['finishedTables'][0]);
729 $this->siteConfig['finishedTables'] = array_values($this->siteConfig['finishedTables']);
730 $this->log(sprintf(__('Search replace for table %s finished', 'backup-backup'), $table), 'SUCCESS');
731 $this->siteConfig['currentSearchReplaceItem'] = 0;
732
733 $processPercentage = intval((($this->siteConfig['amountOfTables'] - sizeof($this->siteConfig['finishedTables'])) / $this->siteConfig['amountOfTables']) * 20);
734 $this->progress(40 + $processPercentage);
735 }
736
737 $this->siteConfig['currentTableTotalUpdates'] = 0;
738 $this->siteConfig['currentSearchReplacePage'] = 0;
739 $this->siteConfig['totalSearchReplacePages'] = 0;
740 $tableCompleted = true;
741 }
742
743 }
744
745 if (!$tableCompleted) break;
746
747 }
748
749 if ($tableCompleted && sizeof($this->siteConfig['finishedTables']) == 0) $this->setContinuation(3, 3); // If path replacement finished
750 else $this->setContinuation(3, 2); // If path raplecement didnt finish, continue next batch (time exceed 5 seconds)
751
752 }
753
754 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 3) $this->setContinuation(4);
755
756 }
757
758 // Step: 4 (summary of S&R and file duplication)
759 private function databaseFinishedCopyFiles() {
760
761 $totalBatchExecution = 5; // 5 seconds
762 $milestoneUpdate = 500; // per 500 files/directories
763 $batchLimit = $milestoneUpdate + 1; // Limit of files/directories per batch
764
765 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 1) {
766
767 // Remove unused variables
768 unset($this->siteConfig['currentSearchReplaceItem']);
769 unset($this->siteConfig['currentTableTotalUpdates']);
770 unset($this->siteConfig['currentSearchReplacePage']);
771 unset($this->siteConfig['totalSearchReplacePages']);
772
773 // Display summary of search replace
774 $this->log(__('Displaying summary of search replace process', 'backup-backup'), 'STEP');
775 $this->log(sprintf(__('Search replace processed %s tables in total', 'backup-backup'), $this->siteConfig['totalTables']));
776 $this->log(sprintf(__('In total it processed %s rows', 'backup-backup'), $this->siteConfig['totalRows']));
777 $this->log(sprintf(__('After all it changed only %s columns', 'backup-backup'), $this->siteConfig['totalChanges']));
778 $this->log(sprintf(__('Which results in %s changes in total of all cells', 'backup-backup'), $this->siteConfig['totalUpdates']));
779
780 // Remove unused variables
781 unset($this->siteConfig['totalTables']);
782 unset($this->siteConfig['totalRows']);
783 unset($this->siteConfig['totalChanges']);
784 unset($this->siteConfig['totalUpdates']);
785
786 // Update user roles in options table
787 $this->updateUserRolesInOptions();
788
789 $this->log(__('Search replace of all tables finished successfully', 'backup-backup'), 'SUCCESS');
790
791 // Go to new process without new batch request
792 $this->siteConfig['batch'] = 2;
793
794 }
795
796 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 2) {
797
798 if (!isset($this->siteConfig['dirSeek']) || $this->siteConfig['dirSeek'] == 0) {
799 $this->log(__('Duplication of directories to staging site', 'backup-backup'), 'STEP');
800 $this->siteConfig['dirSeek'] = 0;
801 }
802
803 $pathDirsListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_directories';
804
805 $file = new \SplFileObject($pathDirsListFile);
806 $file->seek($file->getSize());
807
808 $startTime = time();
809 $currentSeekedElements = 0;
810 $totalLines = $file->key() + 1;
811 $stagingRoot = trailingslashit($this->siteConfig['root_staging']);
812 $file->seek($this->siteConfig['dirSeek']);
813
814 while (!$file->eof()) {
815
816 if ((time() - $startTime) > $totalBatchExecution || $currentSeekedElements > $batchLimit) break;
817
818 $file->seek($this->siteConfig['dirSeek']);
819 $path = $stagingRoot . trim($file->current());
820
821 if (!(file_exists($path) && is_dir($path))) @mkdir($path);
822 if ($this->siteConfig['dirSeek'] % $milestoneUpdate === 0 && $this->siteConfig['dirSeek'] != 0) {
823 $processPercentageTotal = number_format(($this->siteConfig['dirSeek'] / $this->siteConfig['total_directories']) * 100, 2);
824 $this->log(sprintf(
825 __('Directory creation milestone: %s/%s (%s)', 'backup-backup'),
826 $this->siteConfig['dirSeek'],
827 $this->siteConfig['total_directories'],
828 $processPercentageTotal . '%'
829 ));
830 $processPercentage = intval(($this->siteConfig['dirSeek'] / $this->siteConfig['total_directories']) * 5);
831 $this->progress(60 + $processPercentage);
832 }
833
834 $this->siteConfig['dirSeek']++;
835 $currentSeekedElements++;
836
837 }
838
839 if ($this->siteConfig['dirSeek'] >= $this->siteConfig['total_directories']) {
840 unlink($pathDirsListFile);
841 unset($this->siteConfig['dirSeek']);
842 $this->log(sprintf(
843 __('Directory creation milestone: %s/%s (%s)', 'backup-backup'),
844 $this->siteConfig['total_directories'],
845 $this->siteConfig['total_directories'],
846 '100%'
847 ));
848 $this->progress(65);
849 $this->setContinuation(4, 3);
850 } else {
851 $this->setContinuation(4, 2);
852 }
853
854 }
855
856 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 3) {
857
858 if (!isset($this->siteConfig['fileSeek']) || $this->siteConfig['fileSeek'] == 0) {
859 $this->log(__('Duplication of files to staging site', 'backup-backup'), 'STEP');
860 $this->log(__('Duplication of files make take a while', 'backup-backup'));
861 $this->log(__('Disclaimer: ZIP Archives are not getting cloned on staging sites', 'backup-backup'), 'WARN');
862 $this->siteConfig['fileSeek'] = 0;
863 }
864
865 $pathFilesListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_files';
866
867 $file = new \SplFileObject($pathFilesListFile);
868 $file->seek($file->getSize());
869
870 $startTime = time();
871 $currentSeekedElements = 0;
872 $totalLines = $file->key() + 1;
873 $sourceRoot = trailingslashit($this->siteConfig['root_source']);
874 $stagingRoot = trailingslashit($this->siteConfig['root_staging']);
875 $file->seek($this->siteConfig['fileSeek']);
876 $disallowedExtensions = ['zip', 'tar', 'gz', 'tmp', 'rar', '7z'];
877
878 while (!$file->eof()) {
879
880 if ((time() - $startTime) > $totalBatchExecution || $currentSeekedElements > $batchLimit) break;
881
882 $file->seek($this->siteConfig['fileSeek']);
883 $path = trim($file->current());
884
885 if (file_exists($sourceRoot . $path) && !file_exists($stagingRoot . $path)) {
886 $arrayWithExtension = explode('.', $path);
887 $ext = strtolower(array_pop($arrayWithExtension));
888 if (!in_array($ext, $disallowedExtensions) && strpos($path, 'backup-migration-config.php') === false) {
889 @copy($sourceRoot . $path, $stagingRoot . $path);
890 }
891 }
892
893 if ($this->siteConfig['fileSeek'] % $milestoneUpdate === 0 && $this->siteConfig['fileSeek'] != 0) {
894 $processPercentageTotal = number_format(($this->siteConfig['fileSeek'] / $this->siteConfig['total_files']) * 100, 2);
895 $this->log(sprintf(
896 __('File duplication milestone: %s/%s (%s)', 'backup-backup'),
897 $this->siteConfig['fileSeek'],
898 $this->siteConfig['total_files'],
899 $processPercentageTotal . '%'
900 ));
901 $processPercentage = intval(($this->siteConfig['fileSeek'] / $this->siteConfig['total_files']) * 25);
902 $this->progress(65 + $processPercentage);
903 }
904
905 $this->siteConfig['fileSeek']++;
906 $currentSeekedElements++;
907
908 }
909
910 if ($this->siteConfig['fileSeek'] >= $this->siteConfig['total_files']) {
911 unlink($pathFilesListFile);
912 unset($this->siteConfig['fileSeek']);
913 $this->log(sprintf(
914 __('File duplication milestone: %s/%s (%s)', 'backup-backup'),
915 $this->siteConfig['total_files'],
916 $this->siteConfig['total_files'],
917 '100%'
918 ));
919 $this->progress(90);
920 $this->setContinuation(5);
921 } else {
922 $this->setContinuation(4, 3);
923 }
924
925 }
926
927 }
928
929 // Step: 5 (setup passwordless login script and paste adjusted wp-config)
930 private function setupWpConfigAndLoginScript() {
931
932 $this->copyOverPasswordLessScript();
933
934 $this->log(__('Inserting wp-config.php file for staging site', 'backup-backup'), 'STEP');
935
936 $sourceWPConfig = trailingslashit($this->siteConfig['root_source']) . 'wp-config.php';
937 $destinationWPConfig = trailingslashit($this->siteConfig['root_staging']) . 'wp-config.php';
938
939 $previousPrefix = $this->siteConfig['source_db_prefix'];
940 $destinationPrefix = $this->siteConfig['db_prefix'];
941
942 $previousRoot = untrailingslashit($this->siteConfig['root_source']);
943 $destinationRoot = untrailingslashit($this->siteConfig['root_staging']);
944
945 $sourceURL = $this->parseDomain($this->siteConfig['source_home_url']);
946 $destinationURL = $this->parseDomain($this->siteConfig['url']);
947
948 $wpconfig = file_get_contents($sourceWPConfig);
949
950 // Table Prefix
951 $this->log(__('Replacing table prefix in wp-config.php', 'backup-backup'));
952 if (strpos($wpconfig, '"' . $previousPrefix . '";') !== false) {
953 $wpconfig = str_replace('"' . $previousPrefix . '";', '"' . $destinationPrefix . '";', $wpconfig);
954 } elseif (strpos($wpconfig, "'" . $previousPrefix . "';") !== false) {
955 $wpconfig = str_replace("'" . $previousPrefix . "';", "'" . $destinationPrefix . "';", $wpconfig);
956 }
957
958 // Paths e.g. for wp_debug_log
959 $this->log(__('Adjusting paths in wp-config.php', 'backup-backup'));
960 if (strpos($wpconfig, '"' . $previousRoot . '";') !== false) {
961 $wpconfig = str_replace('"' . $previousRoot . '";', '"' . $destinationRoot . '";', $wpconfig);
962 } elseif (strpos($wpconfig, "'" . $previousRoot . "';") !== false) {
963 $wpconfig = str_replace("'" . $previousRoot . "';", "'" . $destinationRoot . "';", $wpconfig);
964 }
965
966 // Paths e.g. for wp_home & wp_siteurl
967 $this->log(__('Adjusting domains in wp-config.php', 'backup-backup'));
968 $wpconfig = explode("\n", $wpconfig);
969 for ($i = 0; $i < sizeof($wpconfig); ++$i) {
970
971 $line = $wpconfig[$i];
972
973 if (strpos($line, 'WP_SITEURL') !== false || strpos($line, 'WP_HOME') !== false) {
974 $wpconfig[$i] = str_replace($sourceURL, $destinationURL, $line);
975 }
976
977 }
978
979 $wpconfig = implode("\n", $wpconfig);
980
981 file_put_contents($destinationWPConfig, $wpconfig);
982 $this->log(__('File adjustments for wp-config.php finished successfully', 'backup-backup'), 'SUCCESS');
983
984 $this->setContinuation(6);
985
986 }
987
988 // Step: 6 (cleanup and misc)
989 private function performFinish() {
990
991 $this->cleanup();
992 $this->setContinuation(7);
993
994 }
995
996 public function __destruct() {
997
998 parent::__destruct();
999
1000 }
1001
1002 }
1003