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