PluginProbe ʕ •ᴥ•ʔ
Backup Migration / 2.1.7
Backup Migration v2.1.7
2.1.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 2 weeks ago local.php 2 weeks ago tastewp.php 2 weeks ago
local.php
1126 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
298 $tables = $wpdb->get_results($wpdb->prepare("SELECT (DATA_LENGTH + INDEX_LENGTH) as `size`, TABLE_NAME AS `name` FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s;", array(DB_NAME)));
299 foreach ($tables as $tableObject) {
300
301 $name = $tableObject->name;
302 $size = $tableObject->size;
303
304 if (substr($name, 0, strlen($table_prefix)) != $table_prefix) {
305 $this->log('Ommiting this table: ' . $name, 'verbose');
306 continue;
307 } else {
308 $this->log('Adding this table: ' . $name, 'verbose');
309 }
310
311 $tableOfStagingSite = false;
312 for ($i = 0; $i < sizeof($currentPrefixes); ++$i) {
313 $subPrefix = $currentPrefixes[$i];
314 if ($table_prefix != $subPrefix && substr($name, 0, strlen($subPrefix)) == $subPrefix) {
315 $tableOfStagingSite = true;
316 break;
317 }
318 }
319 if ($tableOfStagingSite) {
320 $this->log('Excluding this table as part of staging site: ' . $name, 'verbose');
321 continue;
322 }
323
324 $this->siteConfig['total_db_size'] += intval($size);
325 $relatedTables[$name] = $this->siteConfig['db_prefix'] . substr($name, strlen($table_prefix));
326
327 }
328
329 return $relatedTables;
330
331 }
332
333 private function updateUserRolesInOptions() {
334
335 global $wpdb;
336
337 // Update option name
338 $newOptionTable = $this->siteConfig['db_prefix'] . 'options';
339 $newOptionName = $this->siteConfig['db_prefix'] . 'user_roles';
340 $oldOptionName = $this->siteConfig['source_db_prefix'] . 'user_roles';
341
342 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
343 $wpdb->query($wpdb->prepare("UPDATE " . BMP::escapeSQLIDentifier($newOptionTable) . " SET `option_name` = %s WHERE `option_name` = %s;", $newOptionName, $oldOptionName));
344
345 if ($wpdb->last_error !== '') {
346 $translated = __('There was an error during update of user roles:', 'backup-backup') . ' ' . $wpdb->last_error;
347 $english = 'There was an error during update of user roles:' . ' ' . $wpdb->last_error;
348 return $this->returnError($translated, $english);
349 }
350
351 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
352 $wpdb->query("DELETE FROM " . BMP::escapeSQLIDentifier($newOptionTable) . " WHERE `option_name` = 'BMI::STORAGE::LOCAL::PATH';");
353
354 if ($wpdb->last_error !== '') {
355 $translated = __('There was an error during BMI config hard removal:', 'backup-backup') . ' ' . $wpdb->last_error;
356 $english = 'There was an error during BMI config hard removal:' . ' ' . $wpdb->last_error;
357 return $this->returnError($translated, $english);
358 }
359
360 }
361
362 private function duplicateTableAlternative($source, $destination) {
363
364 global $wpdb;
365
366 // Remove failed table if created
367 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
368 $wpdb->query("DROP TABLE IF EXISTS " . BMP::escapeSQLIDentifier($destination) . ";");
369
370 if ($wpdb->last_error !== '') {
371 $translated = __('There was an error during previous destination table removal:', 'backup-backup') . ' ' . $wpdb->last_error;
372 $english = 'There was an error during previous destination table removal:' . ' ' . $wpdb->last_error;
373 return $this->returnError($translated, $english);
374 }
375
376 // Create new table
377 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
378 $wpdb->query("CREATE TABLE " . BMP::escapeSQLIDentifier($destination) . " LIKE " . BMP::escapeSQLIDentifier($source) . ";");
379 if ($wpdb->last_error !== '') {
380 $translated = __('There was an error during database table creation:', 'backup-backup') . ' ' . $wpdb->last_error;
381 $english = 'There was an error during database table creation:' . ' ' . $wpdb->last_error;
382 return $this->returnError($translated, $english);
383 }
384
385 // Duplicate data
386 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
387 $wpdb->query("INSERT INTO " . BMP::escapeSQLIDentifier($destination) . " SELECT * from " . BMP::escapeSQLIDentifier($source) . ";");
388 if ($wpdb->last_error !== '') {
389 $translated = __('There was an error during database table data duplication:', 'backup-backup') . ' ' . $wpdb->last_error;
390 $english = 'There was an error during database table data duplication:' . ' ' . $wpdb->last_error;
391 return $this->returnError($translated, $english);
392 }
393
394 }
395
396 private function duplicateTable($source, $destination) {
397
398 global $wpdb;
399
400 // Create new table
401 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
402 $sourceCreate = $wpdb->get_row("SHOW CREATE TABLE " . BMP::escapeSQLIDentifier($source) . ";");
403 $isTable = isset($sourceCreate->{'Create Table'});
404 if ($isTable) {
405 $sourceCreate->{'Create Table'} = str_ireplace('CREATE TABLE ' . BMP::escapeSQLIDentifier($source), 'CREATE TABLE IF NOT EXISTS ' . BMP::escapeSQLIDentifier($destination), $sourceCreate->{'Create Table'});
406 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL generated from escaped SHOW CREATE TABLE output
407 $wpdb->query($sourceCreate->{'Create Table'});
408 //Insert data
409 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
410 $wpdb->query("INSERT INTO " . BMP::escapeSQLIDentifier($destination) . " SELECT * from " . BMP::escapeSQLIDentifier($source) . ";");
411 } else {
412 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
413 $wpdb->query("CREATE TABLE " . BMP::escapeSQLIDentifier($destination) . " AS SELECT * FROM " . BMP::escapeSQLIDentifier($source) . ";");
414 }
415
416
417
418 if ($wpdb->last_error !== '') {
419 $this->duplicateTableAlternative($source, $destination);
420 }
421
422 if ($wpdb->last_error !== '') {
423 $translated = __('There was an error during database table creation:', 'backup-backup') . ' ' . $wpdb->last_error;
424 $english = 'There was an error during database table creation:' . ' ' . $wpdb->last_error;
425 return $this->returnError($translated, $english);
426 }
427
428 }
429
430 private function parseDomain($domain, $removeWWW = true) {
431
432 if (substr($domain, 0, 8) == 'https://') $domain = substr($domain, 8);
433 if (substr($domain, 0, 7) == 'http://') $domain = substr($domain, 7);
434 if ($removeWWW === true) {
435 if (substr($domain, 0, 4) == 'www.') $domain = substr($domain, 4);
436 }
437 $domain = untrailingslashit($domain);
438
439 return $domain;
440
441 }
442
443 private function performSearchReplace($table, $from, $to, $start = 0, $end = 0, $limitColumns = false) {
444
445 $replaceEngine = new BMISearchReplace([$table], $start, $end, true);
446
447 $stats = $replaceEngine->perform($from, $to, $limitColumns);
448
449 $this->siteConfig['totalTables'] += $stats['tables'];
450 $this->siteConfig['totalRows'] += $stats['rows'];
451 $this->siteConfig['totalChanges'] += $stats['change'];
452 $this->siteConfig['totalUpdates'] += $stats['updates'];
453
454 $this->siteConfig['currentTableTotalUpdates'] += $stats['updates'];
455 $this->siteConfig['currentSearchReplacePage'] = $stats['currentPage'];
456 $this->siteConfig['totalSearchReplacePages'] = $stats['totalPages'];
457
458 $replaceEngine = null;
459
460 }
461
462 public function requestDelete() {
463 return $this->abort(true);
464 }
465
466 public function reconstructConfigurations() {
467
468 global $table_prefix;
469
470 // Load any surviving configuration entries so we preserve them
471 $existingConfig = [];
472 if (file_exists($this->configPath)) {
473 $raw = file_get_contents($this->configPath);
474 $raw = trim(substr($raw, 8));
475 if (is_serialized($raw)) {
476 $parsed = maybe_unserialize($raw);
477 if (is_array($parsed)) $existingConfig = $parsed;
478 }
479 }
480 $this->config = $existingConfig;
481
482 $abspath = trailingslashit(ABSPATH);
483 $reconstructed = [];
484 $skipped = [];
485 $errors = [];
486
487 $entries = @scandir($abspath);
488 if ($entries === false) {
489 return ['status' => 'error', 'message' => 'Cannot read ABSPATH directory.'];
490 }
491
492 foreach ($entries as $entry) {
493
494 if ($entry === '.' || $entry === '..') continue;
495
496 $candidatePath = $abspath . $entry;
497 if (!is_dir($candidatePath)) continue;
498
499 $markerFile = $candidatePath . DIRECTORY_SEPARATOR . '.bmi_staging';
500 if (!file_exists($markerFile)) continue;
501
502 $stagingName = $entry;
503 $stagingRoot = untrailingslashit($candidatePath);
504
505 // Skip sites that already have an intact config file
506 if (isset($this->config[$stagingName]) && isset($this->config[$stagingName]['config'])) {
507 $existingConfigFile = BMI_STAGING . DIRECTORY_SEPARATOR . sanitize_text_field($this->config[$stagingName]['config']) . '.php';
508 if (file_exists($existingConfigFile)) {
509 $skipped[] = $stagingName;
510 continue;
511 }
512 }
513
514 $dbPrefix = null;
515 $wpConfigPath = $stagingRoot . DIRECTORY_SEPARATOR . 'wp-config.php';
516
517 if (file_exists($wpConfigPath) && is_readable($wpConfigPath)) {
518 $wpConfigContent = file_get_contents($wpConfigPath);
519 if (preg_match('/\$table_prefix\s*=\s*[\'"]([^\'"]+)[\'"]\s*;/', $wpConfigContent, $matches)) {
520 $candidate = $matches[1];
521 if ($candidate !== $table_prefix) {
522 $dbPrefix = $candidate;
523 }
524 }
525 }
526
527 if ($dbPrefix === null) {
528 $errors[] = $stagingName;
529 continue;
530 }
531
532 // Build site config
533 $configId = uniqid();
534 $siteConfig = [
535 'name' => $stagingName,
536 'url' => home_url($stagingName),
537 'root_source' => trailingslashit(ABSPATH),
538 'root_staging' => $stagingRoot,
539 'db_prefix' => $dbPrefix,
540 'creation_date' => filemtime($markerFile),
541 'password' => $this->getRandomPassword(),
542 'creator_ip' => $this->getIpAddress(),
543 'login_ip' => $this->getIpAddress(),
544 'login_user_id' => get_current_user_id(),
545 'source_home_url' => home_url(),
546 'source_site_url' => site_url(),
547 'source_db_prefix' => $table_prefix,
548 'communication_secret' => 'local',
549 'expiration_time' => 'never',
550 'step' => 7,
551 'batch' => 1,
552 'total_size' => 'N/A',
553 'total_files' => 'N/A',
554 'total_directories' => 'N/A',
555 'total_db_size' => 'N/A',
556 'amountOfTables' => 'N/A',
557 'sumOfTotalSize' => 'N/A',
558 ];
559
560 $siteConfigPath = BMI_STAGING . DIRECTORY_SEPARATOR . $configId . '.php';
561 file_put_contents($siteConfigPath, '<?php //' . serialize($siteConfig));
562
563 // Register in global config
564 $this->config[$stagingName] = [
565 'config' => $configId,
566 'name' => $stagingName,
567 'prefix' => $dbPrefix,
568 ];
569
570 $reconstructed[] = $stagingName;
571
572 }
573
574 $this->saveConfig();
575
576 return [
577 'status' => (count($errors) > 0 && count($reconstructed) === 0) ? 'error' : 'success',
578 'reconstructed' => $reconstructed,
579 'skipped' => $skipped,
580 'errors' => $errors,
581 ];
582
583 }
584
585 // Step: 0 (initialization of the staging site, create entry)
586 private function initialization() {
587
588 $this->log('Step 0 - Initialization of the process', 'VERBOSE');
589
590 $this->log(__('Name of subsite:', 'backup-backup') . ' ' . $this->name);
591 $this->log(__('Expected URL of subsite:', 'backup-backup') . ' ' . home_url($this->name));
592
593 $this->printInitialLogs();
594
595 $this->createDatabasePrefixAndInitDir();
596 if ($this->wasError) return;
597
598 // Set progress to do something
599 $this->progress(4);
600
601 // Set next step to be 1
602 $this->setContinuation(1);
603
604 }
605
606 // Step: 1 (preparation of file list and database recipes)
607 private function prepareFilesAndDatabase() {
608
609 // Create list of all files and directories
610 if (!isset($this->siteConfig['batch']) || $this->siteConfig['batch'] == 1) {
611 $this->log(__('Scanning all files on your website, it may take a while...', 'backup-backup'), 'STEP');
612
613 $pathDirsListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_directories';
614 $pathFilesListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_files';
615
616 $this->siteConfig['total_size'] = 0;
617 $this->siteConfig['total_files'] = 0;
618 $this->siteConfig['total_directories'] = 0;
619
620 if (file_exists($pathDirsListFile)) @unlink($pathDirsListFile);
621 if (file_exists($pathFilesListFile)) @unlink($pathFilesListFile);
622
623 $this->filesList = fopen($pathFilesListFile, 'a');
624 $this->dirsList = fopen($pathDirsListFile, 'a');
625
626 $topLevelFiles = $this->getAbspathFiles(ABSPATH);
627 $leftDirs = $this->processFiles(trailingslashit(ABSPATH), $topLevelFiles);
628
629 $this->processFilesRecursively($leftDirs);
630
631 fclose($this->filesList);
632 fclose($this->dirsList);
633
634 $this->log(__('All files scanned and prepared for duplication...', 'backup-backup'), 'SUCCESS');
635 $this->progress(10);
636
637 $this->setContinuation(1, 2);
638 }
639
640 // Display details about scan and check database size
641 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 2) {
642 $this->log(__('Looking for database tables that should be duplicated and their sizes...', 'backup-backup'), 'STEP');
643
644 $this->siteConfig['total_db_size'] = 0;
645 $this->siteConfig['tables'] = $this->getTablesForDuplication();
646 $this->siteConfig['amountOfTables'] = sizeof($this->siteConfig['tables']);
647 $this->siteConfig['sumOfTotalSize'] = intval($this->siteConfig['total_db_size']) + intval($this->siteConfig['total_size']);
648
649 $this->log(__('Amount of files to duplicate:', 'backup-backup') . ' ' . $this->siteConfig['total_files']);
650 $this->log(__('Amount of directories to duplicate:', 'backup-backup') . ' ' . $this->siteConfig['total_directories']);
651 $this->log(__('Amount of tables to duplicate:', 'backup-backup') . ' ' . $this->siteConfig['amountOfTables']);
652 $this->log(__('Size of database to duplicate:', 'backup-backup') . ' ' . BMP::humanSize(intval($this->siteConfig['total_db_size'])));
653 $this->log(__('Size of files to duplicate:', 'backup-backup') . ' ' . BMP::humanSize(intval($this->siteConfig['total_size'])));
654 $this->log(__('Total duplication size:', 'backup-backup') . ' ' . BMP::humanSize($this->siteConfig['sumOfTotalSize']));
655 $this->log(__('Tables prepared for duplication...', 'backup-backup'), 'SUCCESS');
656
657 $this->log(__('Checking if there is enough space for the staging site...', 'backup-backup'), 'STEP');
658 $this->log(__('Space checking may take a while', 'backup-backup'));
659
660 $this->progress(15);
661
662 $this->setContinuation(1, 3);
663 }
664
665 // Check if there is enough space for duplication
666 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 3) {
667 require_once BMI_INCLUDES . '/check/checker.php';
668 $checker = new Checker($this->logger);
669
670 $bytes = intval(intval($this->siteConfig['sumOfTotalSize']) * 1.1);
671 $this->log(str_replace('%s2', BMP::humanSize($bytes), str_replace('%s1', $bytes, __('Checking in total: %s1 bytes (%s2)', 'backup-backup'))));
672
673 if (!$checker->check_free_space($bytes, true)) {
674
675 $translated = __('There is not enough space on your server in order to create staging site.', 'backup-backup');
676 $english = 'There is not enough space on your server in order to create staging site.';
677 return $this->returnError($translated, $english);
678
679 } else {
680
681 $this->log(__("Confirmed, there is more than enough space, checked: ", 'backup-backup') . ($bytes) . __(" bytes", 'backup-backup'), 'SUCCESS');
682
683 }
684
685 $this->progress(20);
686
687 // Set next batch to start step 2
688 $this->log('Setting new step for next request to 2 @ batch 1', 'VERBOSE');
689 $this->setContinuation(2);
690
691 }
692
693 }
694
695 // Step: 2 (duplicate database)
696 private function duplicateDatabase() {
697
698 if (!isset($this->siteConfig['finishedTables'])) {
699 $this->siteConfig['finishedTables'] = [];
700 }
701
702 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 1) {
703 $this->log(__('Duplicating database tables...', 'backup-backup'), 'STEP');
704 }
705
706 $startTime = time();
707 foreach ($this->siteConfig['tables'] as $source_table => $destination_table) {
708
709 $this->duplicateTable($source_table, $destination_table);
710 $this->log(str_replace('%s1', $source_table, str_replace('%s2', $destination_table, __('Table %s1 cloned as %s2', 'backup-backup'))));
711
712 $this->siteConfig['finishedTables'][] = $destination_table;
713 unset($this->siteConfig['tables'][$source_table]);
714
715 $processPercentage = intval((sizeof($this->siteConfig['finishedTables']) / $this->siteConfig['amountOfTables']) * 20);
716 $this->progress(20 + $processPercentage);
717
718 if ((time() - $startTime) >= 4) {
719 $this->setContinuation(2, (intval($this->siteConfig['batch']) + 1));
720 break;
721 }
722
723 }
724
725 if (sizeof($this->siteConfig['tables']) == 0) {
726 unset($this->siteConfig['tables']);
727 }
728
729 if (!isset($this->siteConfig['tables'])) {
730 $this->log(__('All tables were successfully duplicated', 'backup-backup'), 'SUCCESS');
731 $this->setContinuation(3);
732 }
733
734 }
735
736 // Step: 3 (search & replace of new tables)
737 private function searchReplace() {
738
739 require_once BMI_INCLUDES . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'search-replace.php';
740
741 // Domain
742 $sourceURL = $this->parseDomain($this->siteConfig['source_home_url']);
743 $destinationURL = $this->parseDomain($this->siteConfig['url']);
744
745 // Paths
746 $sourceABSPATH = untrailingslashit($this->siteConfig['root_source']);
747 $destinationABSPATH = untrailingslashit($this->siteConfig['root_staging']);
748
749 // Items to be replaced
750 $replaces = [
751 ['from' => $sourceABSPATH, 'to' => $destinationABSPATH],
752 ['from' => $sourceURL, 'to' => $destinationURL],
753 ['from' => $this->siteConfig['source_db_prefix'], 'to' => $this->siteConfig['db_prefix']]
754 ];
755
756 $variants = [
757 __("Batch for path adjustment (%s/%s) updated: %s fields.", 'backup-backup'),
758 __("Batch for domain adjustments (%s/%s) updated: %s fields.", 'backup-backup'),
759 __("Batch for prefix key adjustments (%s/%s) updated: %s fields.", 'backup-backup')
760 ];
761
762 $variantsEmpty = [
763 __("Path replacements are not required for table: %s", 'backup-backup'),
764 __("Domain replacements are not required for table: %s", 'backup-backup'),
765 __("Prefix key replacements are not required for table: %s", 'backup-backup')
766 ];
767
768 // Table for replacement
769 if (isset($this->siteConfig['finishedTables']) && sizeof($this->siteConfig['finishedTables']) > 0) {
770
771 // Get first table from duplicated tables
772 $table = $this->siteConfig['finishedTables'][0];
773
774 } else {
775
776 // All tables processed, search replace finished
777 unset($this->siteConfig['finishedTables']);
778 $this->setContinuation(4);
779
780 }
781
782 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 1) {
783 $this->log(__('Performing database search replace on duplicated tables...', 'backup-backup'), 'STEP');
784
785 $pagesize = '?';
786 if (defined('BMI_MAX_SEARCH_REPLACE_PAGE')) $pagesize = BMI_MAX_SEARCH_REPLACE_PAGE;
787 $this->log(__('Page size for that process: ', 'backup-backup') . $pagesize, 'INFO');
788
789 $this->siteConfig['totalTables'] = 0;
790 $this->siteConfig['totalRows'] = 0;
791 $this->siteConfig['totalChanges'] = 0;
792 $this->siteConfig['totalUpdates'] = 0;
793
794 $this->siteConfig['currentSearchReplaceItem'] = 0;
795 $this->siteConfig['currentTableTotalUpdates'] = 0;
796 $this->siteConfig['currentSearchReplacePage'] = 0;
797 $this->siteConfig['totalSearchReplacePages'] = 0;
798
799 $this->setContinuation(3, 2);
800 }
801
802 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 2) {
803
804 // Give details of current process
805 if ($this->siteConfig['currentSearchReplaceItem'] == 0 && $this->siteConfig['currentSearchReplacePage'] == 0) {
806 $this->log(sprintf(__('Performing search replace for table: %s', 'backup-backup'), $table), 'STEP');
807 }
808
809 // Control time of the process
810 $startTime = time();
811 $tableCompleted = false;
812 $maxExecutionTime = 5;
813
814 for ($i = $this->siteConfig['currentSearchReplaceItem']; $i < sizeof($replaces); ++$i) {
815
816 $this->siteConfig['currentSearchReplaceItem'] = $i;
817 if ((time() - $startTime) >= $maxExecutionTime) break;
818 $tableCompleted = false;
819
820 $from = $replaces[$i]['from'];
821 $to = $replaces[$i]['to'];
822
823 while ($tableCompleted === false && (time() - $startTime) < $maxExecutionTime) {
824
825 // Progress
826 $start = $this->siteConfig['currentSearchReplacePage'];
827 $end = $this->siteConfig['totalSearchReplacePages'];
828
829 // Path replace for current table
830 if ($i == 2) {
831 if (strpos($table, 'usermeta') !== false) {
832 $this->performSearchReplace($table, $from, $to, $start, $end, ['meta_key']);
833 }
834 } else {
835 $this->performSearchReplace($table, $from, $to, $start, $end);
836 }
837
838 // Handle batch logs
839 if ($this->siteConfig['currentTableTotalUpdates'] == 0 && $this->siteConfig['totalSearchReplacePages'] == 0) {
840 $this->log(sprintf($variantsEmpty[$i], $table), 'INFO');
841 } else {
842 $st = $this->siteConfig['currentSearchReplacePage'];
843 $en = $this->siteConfig['totalSearchReplacePages'];
844 $this->log(sprintf($variants[$i], $st, $en, $this->siteConfig['currentTableTotalUpdates']));
845 $this->siteConfig['currentTableTotalUpdates'] = 0;
846 }
847
848 // Done of that table
849 if ($this->siteConfig['currentSearchReplacePage'] >= $this->siteConfig['totalSearchReplacePages']) {
850 if ($i >= sizeof($replaces) - 1) {
851 unset($this->siteConfig['finishedTables'][0]);
852 $this->siteConfig['finishedTables'] = array_values($this->siteConfig['finishedTables']);
853 $this->log(sprintf(__('Search replace for table %s finished', 'backup-backup'), $table), 'SUCCESS');
854 $this->siteConfig['currentSearchReplaceItem'] = 0;
855
856 $processPercentage = intval((($this->siteConfig['amountOfTables'] - sizeof($this->siteConfig['finishedTables'])) / $this->siteConfig['amountOfTables']) * 20);
857 $this->progress(40 + $processPercentage);
858 }
859
860 $this->siteConfig['currentTableTotalUpdates'] = 0;
861 $this->siteConfig['currentSearchReplacePage'] = 0;
862 $this->siteConfig['totalSearchReplacePages'] = 0;
863 $tableCompleted = true;
864 }
865
866 }
867
868 if (!$tableCompleted) break;
869
870 }
871
872 if ($tableCompleted && sizeof($this->siteConfig['finishedTables']) == 0) $this->setContinuation(3, 3); // If path replacement finished
873 else $this->setContinuation(3, 2); // If path raplecement didnt finish, continue next batch (time exceed 5 seconds)
874
875 }
876
877 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 3) $this->setContinuation(4);
878
879 }
880
881 // Step: 4 (summary of S&R and file duplication)
882 private function databaseFinishedCopyFiles() {
883
884 $totalBatchExecution = 5; // 5 seconds
885 $milestoneUpdate = 500; // per 500 files/directories
886 $batchLimit = $milestoneUpdate + 1; // Limit of files/directories per batch
887
888 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 1) {
889
890 // Remove unused variables
891 unset($this->siteConfig['currentSearchReplaceItem']);
892 unset($this->siteConfig['currentTableTotalUpdates']);
893 unset($this->siteConfig['currentSearchReplacePage']);
894 unset($this->siteConfig['totalSearchReplacePages']);
895
896 // Display summary of search replace
897 $this->log(__('Displaying summary of search replace process', 'backup-backup'), 'STEP');
898 $this->log(sprintf(__('Search replace processed %s tables in total', 'backup-backup'), $this->siteConfig['totalTables']));
899 $this->log(sprintf(__('In total it processed %s rows', 'backup-backup'), $this->siteConfig['totalRows']));
900 $this->log(sprintf(__('After all it changed only %s columns', 'backup-backup'), $this->siteConfig['totalChanges']));
901 $this->log(sprintf(__('Which results in %s changes in total of all cells', 'backup-backup'), $this->siteConfig['totalUpdates']));
902
903 // Remove unused variables
904 unset($this->siteConfig['totalTables']);
905 unset($this->siteConfig['totalRows']);
906 unset($this->siteConfig['totalChanges']);
907 unset($this->siteConfig['totalUpdates']);
908
909 // Update user roles in options table
910 $this->updateUserRolesInOptions();
911
912 $this->log(__('Search replace of all tables finished successfully', 'backup-backup'), 'SUCCESS');
913
914 // Go to new process without new batch request
915 $this->siteConfig['batch'] = 2;
916
917 }
918
919 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 2) {
920
921 if (!isset($this->siteConfig['dirSeek']) || $this->siteConfig['dirSeek'] == 0) {
922 $this->log(__('Duplication of directories to staging site', 'backup-backup'), 'STEP');
923 $this->siteConfig['dirSeek'] = 0;
924 }
925
926 $pathDirsListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_directories';
927
928 $file = new \SplFileObject($pathDirsListFile);
929 $file->seek($file->getSize());
930
931 $startTime = time();
932 $currentSeekedElements = 0;
933 $totalLines = $file->key() + 1;
934 $stagingRoot = trailingslashit($this->siteConfig['root_staging']);
935 $file->seek($this->siteConfig['dirSeek']);
936
937 while (!$file->eof()) {
938
939 if ((time() - $startTime) > $totalBatchExecution || $currentSeekedElements > $batchLimit) break;
940
941 $file->seek($this->siteConfig['dirSeek']);
942 $path = $stagingRoot . trim($file->current());
943
944 if (!(file_exists($path) && is_dir($path))) @mkdir($path);
945 if ($this->siteConfig['dirSeek'] % $milestoneUpdate === 0 && $this->siteConfig['dirSeek'] != 0) {
946 $processPercentageTotal = number_format(($this->siteConfig['dirSeek'] / $this->siteConfig['total_directories']) * 100, 2);
947 $this->log(sprintf(
948 __('Directory creation milestone: %s/%s (%s)', 'backup-backup'),
949 $this->siteConfig['dirSeek'],
950 $this->siteConfig['total_directories'],
951 $processPercentageTotal . '%'
952 ));
953 $processPercentage = intval(($this->siteConfig['dirSeek'] / $this->siteConfig['total_directories']) * 5);
954 $this->progress(60 + $processPercentage);
955 }
956
957 $this->siteConfig['dirSeek']++;
958 $currentSeekedElements++;
959
960 }
961
962 if ($this->siteConfig['dirSeek'] >= $this->siteConfig['total_directories']) {
963 unlink($pathDirsListFile);
964 unset($this->siteConfig['dirSeek']);
965 $this->log(sprintf(
966 __('Directory creation milestone: %s/%s (%s)', 'backup-backup'),
967 $this->siteConfig['total_directories'],
968 $this->siteConfig['total_directories'],
969 '100%'
970 ));
971 $this->progress(65);
972 $this->setContinuation(4, 3);
973 } else {
974 $this->setContinuation(4, 2);
975 }
976
977 }
978
979 if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 3) {
980
981 if (!isset($this->siteConfig['fileSeek']) || $this->siteConfig['fileSeek'] == 0) {
982 $this->log(__('Duplication of files to staging site', 'backup-backup'), 'STEP');
983 $this->log(__('Duplication of files make take a while', 'backup-backup'));
984 $this->log(__('Disclaimer: ZIP Archives are not getting cloned on staging sites', 'backup-backup'), 'WARN');
985 $this->siteConfig['fileSeek'] = 0;
986 }
987
988 $pathFilesListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_files';
989
990 $file = new \SplFileObject($pathFilesListFile);
991 $file->seek($file->getSize());
992
993 $startTime = time();
994 $currentSeekedElements = 0;
995 $totalLines = $file->key() + 1;
996 $sourceRoot = trailingslashit($this->siteConfig['root_source']);
997 $stagingRoot = trailingslashit($this->siteConfig['root_staging']);
998 $file->seek($this->siteConfig['fileSeek']);
999 $disallowedExtensions = ['zip', 'tar', 'gz', 'tmp', 'rar', '7z'];
1000
1001 while (!$file->eof()) {
1002
1003 if ((time() - $startTime) > $totalBatchExecution || $currentSeekedElements > $batchLimit) break;
1004
1005 $file->seek($this->siteConfig['fileSeek']);
1006 $path = trim($file->current());
1007
1008 if (file_exists($sourceRoot . $path) && !file_exists($stagingRoot . $path)) {
1009 $arrayWithExtension = explode('.', $path);
1010 $ext = strtolower(array_pop($arrayWithExtension));
1011 if (!in_array($ext, $disallowedExtensions) && strpos($path, 'backup-migration-config.php') === false) {
1012 @copy($sourceRoot . $path, $stagingRoot . $path);
1013 }
1014 }
1015
1016 if ($this->siteConfig['fileSeek'] % $milestoneUpdate === 0 && $this->siteConfig['fileSeek'] != 0) {
1017 $processPercentageTotal = number_format(($this->siteConfig['fileSeek'] / $this->siteConfig['total_files']) * 100, 2);
1018 $this->log(sprintf(
1019 __('File duplication milestone: %s/%s (%s)', 'backup-backup'),
1020 $this->siteConfig['fileSeek'],
1021 $this->siteConfig['total_files'],
1022 $processPercentageTotal . '%'
1023 ));
1024 $processPercentage = intval(($this->siteConfig['fileSeek'] / $this->siteConfig['total_files']) * 25);
1025 $this->progress(65 + $processPercentage);
1026 }
1027
1028 $this->siteConfig['fileSeek']++;
1029 $currentSeekedElements++;
1030
1031 }
1032
1033 if ($this->siteConfig['fileSeek'] >= $this->siteConfig['total_files']) {
1034 unlink($pathFilesListFile);
1035 unset($this->siteConfig['fileSeek']);
1036 $this->log(sprintf(
1037 __('File duplication milestone: %s/%s (%s)', 'backup-backup'),
1038 $this->siteConfig['total_files'],
1039 $this->siteConfig['total_files'],
1040 '100%'
1041 ));
1042 $this->progress(90);
1043 $this->setContinuation(5);
1044 } else {
1045 $this->setContinuation(4, 3);
1046 }
1047
1048 }
1049
1050 }
1051
1052 // Step: 5 (setup passwordless login script and paste adjusted wp-config)
1053 private function setupWpConfigAndLoginScript() {
1054
1055 $this->copyOverPasswordLessScript();
1056
1057 $this->log(__('Inserting wp-config.php file for staging site', 'backup-backup'), 'STEP');
1058
1059 $sourceWPConfig = trailingslashit($this->siteConfig['root_source']) . 'wp-config.php';
1060 $destinationWPConfig = trailingslashit($this->siteConfig['root_staging']) . 'wp-config.php';
1061
1062 $previousPrefix = $this->siteConfig['source_db_prefix'];
1063 $destinationPrefix = $this->siteConfig['db_prefix'];
1064
1065 $previousRoot = untrailingslashit($this->siteConfig['root_source']);
1066 $destinationRoot = untrailingslashit($this->siteConfig['root_staging']);
1067
1068 $sourceURL = $this->parseDomain($this->siteConfig['source_home_url']);
1069 $destinationURL = $this->parseDomain($this->siteConfig['url']);
1070
1071 $wpconfig = file_get_contents($sourceWPConfig);
1072
1073 // Table Prefix
1074 $this->log(__('Replacing table prefix in wp-config.php', 'backup-backup'));
1075 if (strpos($wpconfig, '"' . $previousPrefix . '";') !== false) {
1076 $wpconfig = str_replace('"' . $previousPrefix . '";', '"' . $destinationPrefix . '";', $wpconfig);
1077 } elseif (strpos($wpconfig, "'" . $previousPrefix . "';") !== false) {
1078 $wpconfig = str_replace("'" . $previousPrefix . "';", "'" . $destinationPrefix . "';", $wpconfig);
1079 }
1080
1081 // Paths e.g. for wp_debug_log
1082 $this->log(__('Adjusting paths in wp-config.php', 'backup-backup'));
1083 if (strpos($wpconfig, '"' . $previousRoot . '";') !== false) {
1084 $wpconfig = str_replace('"' . $previousRoot . '";', '"' . $destinationRoot . '";', $wpconfig);
1085 } elseif (strpos($wpconfig, "'" . $previousRoot . "';") !== false) {
1086 $wpconfig = str_replace("'" . $previousRoot . "';", "'" . $destinationRoot . "';", $wpconfig);
1087 }
1088
1089 // Paths e.g. for wp_home & wp_siteurl
1090 $this->log(__('Adjusting domains in wp-config.php', 'backup-backup'));
1091 $wpconfig = explode("\n", $wpconfig);
1092 for ($i = 0; $i < sizeof($wpconfig); ++$i) {
1093
1094 $line = $wpconfig[$i];
1095
1096 if (strpos($line, 'WP_SITEURL') !== false || strpos($line, 'WP_HOME') !== false) {
1097 $wpconfig[$i] = str_replace($sourceURL, $destinationURL, $line);
1098 }
1099
1100 }
1101
1102 $wpconfig = implode("\n", $wpconfig);
1103
1104 file_put_contents($destinationWPConfig, $wpconfig);
1105 $this->log(__('File adjustments for wp-config.php finished successfully', 'backup-backup'), 'SUCCESS');
1106
1107 $this->setContinuation(6);
1108
1109 }
1110
1111 // Step: 6 (cleanup and misc)
1112 private function performFinish() {
1113
1114 $this->cleanup();
1115 $this->setContinuation(7);
1116
1117 }
1118
1119 public function __destruct() {
1120
1121 parent::__destruct();
1122
1123 }
1124
1125 }
1126