PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.11.1
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.11.1
4.11.2 4.11.1 4.11.0 4.10.0 4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Backend / Modules / Jobs / Cloning.php
wp-staging / Backend / Modules / Jobs Last commit date
Cleaners 1 week ago Exceptions 1 week ago Cancel.php 1 week ago CancelUpdate.php 1 week ago Cloning.php 1 week ago CloningProcess.php 1 week ago Data.php 1 week ago Database.php 1 week ago Delete.php 1 week ago Directories.php 1 week ago Files.php 1 week ago Finish.php 1 week ago Job.php 1 week ago JobExecutable.php 1 week ago Logs.php 1 week ago PreserveDataFirstStep.php 1 week ago PreserveDataSecondStep.php 1 week ago ProcessLock.php 1 week ago Scan.php 1 week ago SearchReplace.php 1 week ago TotalStepsAreNumberOfTables.php 1 week ago Updating.php 1 week ago
Cloning.php
734 lines
1 <?php
2
3 namespace WPStaging\Backend\Modules\Jobs;
4
5 use Countable;
6 use Exception;
7 use WPStaging\Backend\Modules\Jobs\Exceptions\JobNotFoundException;
8 use WPStaging\Backup\Service\Database\DatabaseImporter;
9 use WPStaging\Core\WPStaging;
10 use WPStaging\Framework\Analytics\Actions\AnalyticsStagingCreate;
11 use WPStaging\Framework\Traits\TablePrefixValidator;
12 use WPStaging\Framework\Database\SelectedTables;
13 use WPStaging\Framework\Exceptions\WPStagingException;
14 use WPStaging\Framework\Filesystem\PathIdentifier;
15 use WPStaging\Framework\Filesystem\Scanning\ScanConst;
16 use WPStaging\Framework\Security\AccessToken;
17 use WPStaging\Framework\Utils\Urls;
18 use WPStaging\Framework\Utils\Sanitize;
19 use WPStaging\Framework\Facades\Hooks;
20 use WPStaging\Framework\Utils\Strings;
21 use WPStaging\Framework\Utils\WpDefaultDirectories;
22 use WPStaging\Staging\Sites;
23
24 use function WPStaging\functions\debug_log;
25
26
27
28
29
30 class Cloning extends Job
31 {
32 use TablePrefixValidator;
33
34
35
36
37 const WPSTG_REQUEST = 'wpstg_cloning';
38
39
40 const FILTER_CLONE_EXCLUDED_FILES_FULL_PATH = 'wpstg.clone.excluded_files_full_path';
41
42
43 const FILTER_CLONE_EXCLUDED_FILES = 'wpstg_clone_excluded_files';
44
45
46
47
48 private $db;
49
50
51
52
53 private $dirUtils;
54
55
56
57
58 private $sitesHelper;
59
60
61
62
63 private $errorMessage;
64
65
66
67
68 protected $sanitize;
69
70
71
72
73 private $urls;
74
75
76 private $pathIdentifier;
77
78
79 protected $strUtil;
80
81
82
83
84 public function initialize()
85 {
86 $this->db = WPStaging::getInstance()->get("wpdb");
87 $this->dirUtils = new WpDefaultDirectories();
88 $this->sitesHelper = new Sites();
89 $this->sanitize = WPStaging::make(Sanitize::class);
90 $this->urls = WPStaging::make(Urls::class);
91 $this->strUtil = WPStaging::make(Strings::class);
92 $this->pathIdentifier = WPStaging::make(PathIdentifier::class);
93 }
94
95 public function getErrorMessage(): string
96 {
97 return $this->errorMessage;
98 }
99
100
101
102
103
104
105 public function save(): bool
106 {
107 if (!isset($_POST) || !isset($_POST["cloneID"])) {
108 $this->errorMessage = __("clone ID missing", 'wp-staging');
109 return false;
110 }
111
112
113 $this->filesIndexCache->delete();
114
115
116 $this->options->root = str_replace(["\\", '/'], DIRECTORY_SEPARATOR, ABSPATH);
117 $this->options->current = null;
118 $this->options->currentClone = null;
119
120
121 $this->options->clone = preg_replace("#\W+#", '-', strtolower($this->sanitize->sanitizeString($_POST["cloneID"])));
122
123
124 if (isset($_POST["cloneName"])) {
125 $this->options->cloneName = sanitize_text_field($_POST["cloneName"]);
126 }
127
128
129 if (empty($this->options->cloneName) || $this->options->cloneName === $this->options->clone) {
130 $this->options->cloneName = $this->maybeGenerateFriendlyName();
131 }
132
133
134 $this->options->cloneDirectoryName = $this->sitesHelper->sanitizeDirectoryName($this->options->cloneName);
135 $result = $this->sitesHelper->isCloneExists($this->options->cloneDirectoryName);
136 if ($result !== false) {
137 $this->errorMessage = $result;
138 return false;
139 }
140
141 $this->options->cloneNumber = 1;
142 $this->options->prefix = $this->setStagingPrefix();
143 $this->options->includedDirectories = [];
144 $this->options->excludedDirectories = [];
145 $this->options->extraDirectories = [];
146 $this->options->excludedFiles = Hooks::applyFilters(self::FILTER_CLONE_EXCLUDED_FILES, [
147 '.DS_Store',
148 '*.git',
149 '*.svn',
150 '*.tmp',
151 'desktop.ini',
152 '.gitignore',
153 '*.log',
154 'web.config',
155 '.wp-staging',
156 '.wp-staging-cloneable',
157 ]);
158
159 $excludedFilesFullPath = [
160 '.htaccess',
161 PathIdentifier::IDENTIFIER_WP_CONTENT . 'db.php',
162 PathIdentifier::IDENTIFIER_WP_CONTENT . 'object-cache.php',
163 PathIdentifier::IDENTIFIER_WP_CONTENT . 'advanced-cache.php',
164 ];
165
166 $hostingExclusions = $this->getHostingProviderExclusions();
167 $this->options->tmpExcludedHostingFiles = $hostingExclusions['absolutePaths'];
168 $excludedFilesFullPath = array_merge($excludedFilesFullPath, $hostingExclusions['files']);
169
170 $this->options->excludedFilesFullPath = Hooks::applyFilters(self::FILTER_CLONE_EXCLUDED_FILES_FULL_PATH, $excludedFilesFullPath);
171
172 $this->options->currentStep = 0;
173
174
175 $this->options->job = new \stdClass();
176 $this->loadLegacyExistingClones();
177
178
179 if (isset($this->options->existingClones[$this->options->clone])) {
180 $existingClone = (array)$this->options->existingClones[$this->options->clone];
181 $this->options->cloneNumber = isset($existingClone['number']) ? (int)$existingClone['number'] : 1;
182 $this->options->prefix = !empty($existingClone['prefix']) && is_string($existingClone['prefix']) ? $existingClone['prefix'] : $this->setStagingPrefix();
183
184
185
186 } elseif (!empty($this->options->existingClones)) {
187 $this->options->cloneNumber = count($this->options->existingClones) + 1;
188 }
189
190 $this->options->networkClone = false;
191 if ($this->isMultisiteAndPro() && is_main_site()) {
192 $this->options->networkClone = isset($_POST['networkClone']) && $this->sanitize->sanitizeBool($_POST['networkClone']);
193 }
194
195
196 $includedTables = isset($_POST['includedTables']) ? $this->sanitize->sanitizeString($_POST['includedTables']) : '';
197 $excludedTables = isset($_POST['excludedTables']) ? $this->sanitize->sanitizeString($_POST['excludedTables']) : '';
198 $selectedTablesWithoutPrefix = isset($_POST['selectedTablesWithoutPrefix']) ? $this->sanitize->sanitizeString($_POST['selectedTablesWithoutPrefix']) : '';
199 $selectedTables = new SelectedTables($includedTables, $excludedTables, $selectedTablesWithoutPrefix);
200 $selectedTables->setAllTablesExcluded(empty($_POST['allTablesExcluded']) ? false : $this->sanitize->sanitizeBool($_POST['allTablesExcluded']));
201 $this->options->tables = $selectedTables->getSelectedTables($this->options->networkClone);
202
203
204 $this->options->excludeGlobRules = [];
205 if (!empty($_POST["excludeGlobRules"])) {
206 $this->options->excludeGlobRules = $this->sanitize->sanitizeExcludeRules($_POST["excludeGlobRules"]);
207 }
208
209
210 $this->options->excludeSizeRules = [];
211 if (!empty($_POST["excludeSizeRules"])) {
212 $this->options->excludeSizeRules = $this->sanitize->sanitizeExcludeRules($_POST["excludeSizeRules"]);
213 }
214
215 $this->options->uploadsSymlinked = isset($_POST['uploadsSymlinked']) && $this->sanitize->sanitizeBool($_POST['uploadsSymlinked']);
216
217 $pluginWpContentDir = rtrim($this->directoryAdapter->getPluginWpContentDirectory(), '/\\');
218
219
220
221
222
223 $excludedDirectories = [
224 PathIdentifier::IDENTIFIER_WP_CONTENT . 'cache',
225 $this->pathIdentifier->transformPathToIdentifiable($pluginWpContentDir),
226 PathIdentifier::IDENTIFIER_WP_CONTENT . WPSTG_PLUGIN_DOMAIN,
227 ];
228
229 $excludedDirectories = array_merge($excludedDirectories, $hostingExclusions['directories']);
230
231
232 if ($this->options->uploadsSymlinked) {
233 $excludedDirectories[] = PathIdentifier::IDENTIFIER_UPLOADS;
234 }
235
236 $excludedDirectoriesRequest = isset($_POST["excludedDirectories"]) ? $this->sanitize->sanitizeString($_POST["excludedDirectories"]) : '';
237 $excludedDirectoriesRequest = $this->dirUtils->getExcludedDirectories($excludedDirectoriesRequest);
238
239 $this->options->excludedDirectories = array_merge($excludedDirectories, $excludedDirectoriesRequest);
240
241
242 if (isset($_POST["extraDirectories"])) {
243 $this->options->extraDirectories = explode(ScanConst::DIRECTORIES_SEPARATOR, $this->sanitize->sanitizeString($_POST["extraDirectories"]));
244 }
245
246
247 $this->options->useNewAdminAccount = false;
248 $this->options->adminEmail = '';
249 $this->options->adminPassword = '';
250
251
252 $this->options->databaseServer = 'localhost';
253 $this->options->databaseUser = '';
254 $this->options->databasePassword = '';
255 $this->options->databaseDatabase = '';
256
257 $this->options->databasePrefix = $this->isExternalDatabase() ? $this->db->prefix : '';
258 $this->options->databaseSsl = false;
259
260
261 $this->options->cloneDir = '';
262 $this->options->cloneHostname = '';
263
264
265 $this->options->isEmailsAllowed = true;
266 $this->options->isCronEnabled = true;
267 $this->options->isWooSchedulerEnabled = true;
268 $this->options->isEmailsReminderEnabled = false;
269 $this->options->isAutoUpdatePlugins = false;
270 $this->setAdvancedCloningOptions();
271
272 $this->options->destinationDir = $this->getDestinationDir();
273 $this->options->destinationHostname = $this->getDestinationHostname();
274
275 $this->options->homeHostname = $this->urls->getHomeUrlWithoutScheme();
276
277
278 $this->options->isRunning = true;
279 $this->initializeLegacyStagingRun(Job::STAGING);
280
281
282 $this->options->ownerId = get_current_user_id();
283
284 $this->saveClone();
285
286 if (!$this->saveOptions()) {
287 return false;
288 }
289
290 WPStaging::make(AnalyticsStagingCreate::class)->enqueueStartEvent($this->options->jobIdentifier, $this->options);
291 $this->errorMessage = "";
292 return true;
293 }
294
295
296
297
298
299 private function saveClone()
300 {
301
302 $this->debugLog("Cloning: {$this->options->clone}'s clone job's data is not in database, generating data");
303
304 $this->options->existingClones[$this->options->clone] = [
305 "cloneName" => $this->options->cloneName,
306 "directoryName" => $this->options->cloneDirectoryName,
307 "path" => trailingslashit($this->options->destinationDir),
308 "url" => $this->getDestinationUrl(),
309 "number" => $this->options->cloneNumber,
310 "version" => WPStaging::getVersion(),
311 "status" => "unfinished or broken (?)",
312 "prefix" => $this->options->prefix,
313 "datetime" => time(),
314 "useCustomDatabase" => $this->isExternalDatabase(),
315 "databaseUser" => $this->options->databaseUser,
316 "databasePassword" => $this->options->databasePassword,
317 "databaseDatabase" => $this->options->databaseDatabase,
318 "databaseServer" => $this->options->databaseServer,
319 "databasePrefix" => $this->options->databasePrefix,
320 "databaseSsl" => (bool)$this->options->databaseSsl,
321 "isCronEnabled" => (bool)$this->options->isCronEnabled,
322 "isEmailsAllowed" => (bool)$this->options->isEmailsAllowed,
323 "uploadsSymlinked" => (bool)$this->options->uploadsSymlinked,
324 "ownerId" => $this->options->ownerId,
325 "includedTables" => $this->options->tables,
326 "excludeSizeRules" => $this->options->excludeSizeRules,
327 "excludeGlobRules" => $this->options->excludeGlobRules,
328 "excludedDirectories" => $this->options->excludedDirectories,
329 "extraDirectories" => $this->options->extraDirectories,
330 "networkClone" => $this->isNetworkClone(),
331 'useNewAdminAccount' => $this->options->useNewAdminAccount,
332 'adminEmail' => $this->options->adminEmail,
333 'adminPassword' => $this->options->adminPassword,
334 'isWooSchedulerEnabled' => (bool)$this->options->isWooSchedulerEnabled,
335 "isEmailsReminderEnabled" => (bool)$this->options->isEmailsReminderEnabled,
336 'isAutoUpdatePlugins' => (bool)$this->options->isAutoUpdatePlugins,
337 ];
338
339 if ($this->sitesHelper->updateStagingSites($this->options->existingClones) === false) {
340 $this->log("Cloning: Failed to save {$this->options->clone}'s clone job data to database'");
341 }
342 }
343
344
345
346
347
348 private function getDestinationUrl(): string
349 {
350 if (!empty($this->options->cloneHostname)) {
351 return $this->options->cloneHostname;
352 }
353
354 return trailingslashit(get_site_url()) . $this->options->cloneDirectoryName;
355 }
356
357
358
359
360
361 private function getDestinationHostname(): string
362 {
363 if (empty($this->options->cloneHostname)) {
364 return $this->urls->getHomeUrlWithoutScheme();
365 }
366
367 return $this->getHostnameWithoutScheme($this->options->cloneHostname);
368 }
369
370
371
372
373
374
375 private function getHostnameWithoutScheme(string $string): string
376 {
377 return preg_replace('#^https?://#', '', rtrim($string, '/'));
378 }
379
380
381
382
383
384 private function getDestinationDir(): string
385 {
386
387 if (!empty($this->options->cloneDir) & (trailingslashit($this->options->cloneDir) === trailingslashit(WPStaging::getWPpath()))) {
388 $this->returnException('Error: Target path must be different from the root of the production website.');
389 }
390
391
392 if (!empty($this->options->cloneDir)) {
393 return trailingslashit($this->options->cloneDir);
394 }
395
396
397 $cloneDestinationPath = $this->directoryAdapter->getAbsPath() . $this->options->cloneDirectoryName;
398
399 if (!is_writable($this->directoryAdapter->getAbsPath())) {
400 $stagingSiteDirectory = $this->directoryAdapter->getStagingSiteDirectoryInsideWpcontent();
401 if ($stagingSiteDirectory === false) {
402 debug_log(esc_html('Fail to get destination directory. The staging sites destination folder cannot be created.'));
403 $this->returnException('The staging sites directory is not writable. Please choose another path.');
404 }
405
406 $cloneDestinationPath = trailingslashit($stagingSiteDirectory) . $this->options->cloneDirectoryName;
407 if (empty($this->options->cloneHostname)) {
408 $this->options->cloneHostname = trailingslashit($this->directoryAdapter->getStagingSiteUrl()) . $this->options->cloneDirectoryName;
409 }
410 }
411
412 $this->options->cloneDir = trailingslashit($cloneDestinationPath);
413 return $this->options->cloneDir;
414 }
415
416
417
418
419 private function setStagingPrefix()
420 {
421
422
423 for ($i = 0; $i <= 10000; $i++) {
424 $this->options->prefix = !empty($this->options->existingClones) && $this->options->existingClones instanceof Countable
425 ? 'wpstg' . (count($this->options->existingClones) + $i) . '_'
426 : 'wpstg' . $i . '_';
427
428 $sql = "SHOW TABLE STATUS LIKE '{$this->options->prefix}%'";
429 $tables = $this->db->get_results($sql);
430
431
432 if (!$tables) {
433 return $this->options->prefix;
434 }
435 }
436
437 $message = sprintf("Fatal Error: Can not create staging prefix. '%s' already exists! Stopping for security reasons. Contact support@wp-staging.com", $this->options->prefix);
438 $this->returnException($message);
439 wp_die(esc_html($message));
440 }
441
442
443
444
445
446
447 public function start()
448 {
449 if (!is_object($this->options)) {
450 return;
451 }
452
453 if (!property_exists($this->options, 'currentJob') || $this->options->currentJob === null) {
454 $this->log("Cloning job finished");
455 return true;
456 }
457
458 $methodName = "job" . ucwords($this->options->currentJob);
459
460 if (!method_exists($this, $methodName)) {
461 $this->log("Can't execute job; Job's method $methodName is not found");
462 throw new JobNotFoundException($methodName);
463 }
464
465 if ($this->options->databasePrefix === $this->db->prefix && $this->isStagingDatabaseSameAsProductionDatabase()) {
466 $this->returnException('Table prefix for staging site can not be identical to live database if staging site will be cloned into production database! Please start over and change the table prefix or destination database.');
467 }
468
469 if (defined('WPSTG_IS_DEV') && WPSTG_IS_DEV === true) {
470 return $this->{$methodName}();
471 }
472
473 $tmpPrefixes = [
474 DatabaseImporter::TMP_DATABASE_PREFIX,
475 DatabaseImporter::TMP_DATABASE_PREFIX_TO_DROP,
476 ];
477
478 if (in_array($this->options->databasePrefix, $tmpPrefixes)) {
479 $this->returnException('Prefix wpstgtmp_ and wpstgbak_ are preserved by WP Staging and cannot be used for CLONING purpose! Please start over and change the table prefix.');
480 }
481
482 if ($this->isWpStagingReservedPrefix($this->options->databasePrefix)) {
483 $this->returnException($this->getReservedPrefixErrorMessage($this->options->databasePrefix));
484 }
485
486
487 return $this->{$methodName}();
488 }
489
490
491
492
493
494
495
496 private function handleJobResponse($response, string $nextJob)
497 {
498
499 if ($response->status !== true) {
500 return $response;
501 }
502
503 $this->options->job = new \stdClass();
504 $this->options->currentJob = $nextJob;
505 $this->options->currentStep = 0;
506 $this->options->totalSteps = 0;
507
508
509 $this->saveOptions();
510
511 return $response;
512 }
513
514
515
516
517
518
519 public function jobPreserveDataFirstStep()
520 {
521 $this->writeJobSpecificLogStartHeader();
522
523 $preserve = new PreserveDataFirstStep();
524 return $this->handleJobResponse($preserve->start(), 'database');
525 }
526
527
528
529
530
531
532 public function jobDatabase()
533 {
534 $database = new Database();
535 return $this->handleJobResponse($database->start(), "SearchReplace");
536 }
537
538
539
540
541
542
543 public function jobSearchReplace()
544 {
545 $searchReplace = new SearchReplace();
546 return $this->handleJobResponse($searchReplace->start(), "PreserveDataSecondStep");
547 }
548
549
550
551
552
553
554 public function jobPreserveDataSecondStep()
555 {
556 $preserve = new PreserveDataSecondStep();
557 return $this->handleJobResponse($preserve->start(), 'directories');
558 }
559
560
561
562
563
564
565 public function jobDirectories()
566 {
567 $directories = new Directories();
568 return $this->handleJobResponse($directories->start(), "files");
569 }
570
571
572
573
574
575
576 public function jobFiles()
577 {
578 $files = new Files();
579 return $this->handleJobResponse($files->start(), "data");
580 }
581
582
583
584
585
586
587 public function jobData()
588 {
589 $dataJob = $this->getDataJob();
590 return $this->handleJobResponse($dataJob->start(), "finish");
591 }
592
593
594
595
596
597
598 public function jobFinish()
599 {
600
601
602
603 $accessToken = new AccessToken();
604 $accessToken->generateNewToken();
605
606 $finish = new Finish();
607 return $this->handleJobResponse($finish->start(), '');
608 }
609
610
611
612
613 public function getDataJob(): Data
614 {
615 return new Data();
616 }
617
618
619
620
621 protected function setAdvancedCloningOptions()
622 {
623
624 }
625
626
627
628
629 private function writeJobSpecificLogStartHeader()
630 {
631
632 $jobName = empty($this->options->mainJob) ? 'Unknown' : $this->options->mainJob;
633
634 switch ($jobName) {
635 case Job::UPDATE:
636 $jobName = 'Update';
637 break;
638 case Job::RESET:
639 $jobName = 'Reset';
640 break;
641 case Job::STAGING:
642 $jobName = 'Cloning';
643 break;
644 default:
645 $jobName = 'Unknown';
646 break;
647 }
648
649 $this->log('#################### Start ' . $jobName . ' Job ####################', 'INFO');
650 if ($jobName !== 'Cloning' && !empty($this->options->clone)) {
651 $this->logger->info(esc_html('Staging Site ID: ' . $this->options->clone));
652 $this->logger->info(esc_html('Staging Site: ' . $this->options->cloneName));
653 }
654
655 $this->logger->writeLogHeader();
656 $this->logger->writeInstalledPluginsAndThemes();
657 $this->addJobSettingsToLogs($jobName);
658 }
659
660
661
662
663
664 private function maybeGenerateFriendlyName(): string
665 {
666
667 $nameList = [
668 "enterprise",
669 "voyager",
670 "defiant",
671 "discovery",
672 "excelsior",
673 "intrepid",
674 "constitution",
675 "reliant",
676 "grissom",
677 "yamato",
678 "excelsior",
679 "venture",
680 "cerritos",
681 "prometheus",
682 "bellerophon",
683 "sanpablo",
684 "sutherland",
685 "shenzhou",
686 "titan",
687 "reliant",
688 "stargazer",
689 "franklin",
690 "protostar",
691 ];
692
693
694 shuffle($nameList);
695
696
697 $stagingSites = $this->sitesHelper->tryGettingStagingSites();
698 foreach ($nameList as $name) {
699
700 $name = sanitize_text_field($name);
701 $dirPath = ABSPATH . $name;
702
703 if (file_exists($dirPath)) {
704 continue;
705 }
706
707
708 if (!$this->isStagingSiteNameExists($name, $stagingSites)) {
709 return $name;
710 }
711 }
712
713
714 return (string)$this->options->clone;
715 }
716
717
718
719
720
721
722
723 private function isStagingSiteNameExists(string $name, array $stagingSites): bool
724 {
725 foreach ($stagingSites as $site) {
726 if ($site['directoryName'] === $name) {
727 return true;
728 }
729 }
730
731 return false;
732 }
733 }
734