PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.9.2
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.9.2
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 3 months ago Exceptions 5 years ago Cancel.php 7 months ago CancelUpdate.php 7 months ago Cloning.php 2 weeks ago CloningProcess.php 1 year ago Data.php 7 months ago Database.php 3 months ago Delete.php 4 months ago Directories.php 4 months ago Files.php 1 week ago Finish.php 2 months ago Job.php 2 weeks ago JobExecutable.php 7 months ago Logs.php 3 years ago PreserveDataFirstStep.php 1 month ago PreserveDataSecondStep.php 1 month ago ProcessLock.php 1 year ago Scan.php 4 months ago SearchReplace.php 5 months ago TotalStepsAreNumberOfTables.php 5 years ago Updating.php 2 weeks 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 * Class Cloning
29 * @package WPStaging\Backend\Modules\Jobs
30 */
31 class Cloning extends Job
32 {
33 use TablePrefixValidator;
34
35 /**
36 * @var string
37 */
38 const WPSTG_REQUEST = 'wpstg_cloning';
39
40 /** @var string */
41 const FILTER_CLONE_EXCLUDED_FILES_FULL_PATH = 'wpstg.clone.excluded_files_full_path';
42
43 /** @var string */
44 const FILTER_CLONE_EXCLUDED_FILES = 'wpstg_clone_excluded_files';
45
46 /**
47 * @var object
48 */
49 private $db;
50
51 /**
52 * @var WpDefaultDirectories
53 */
54 private $dirUtils;
55
56 /**
57 * @var Sites
58 */
59 private $sitesHelper;
60
61 /**
62 * @var string
63 */
64 private $errorMessage;
65
66 /**
67 * @var Sanitize
68 */
69 protected $sanitize;
70
71 /**
72 * @var Urls
73 */
74 private $urls;
75
76 /** @var Directory */
77 private $dirAdapter;
78
79 /** @var PathIdentifier */
80 private $pathIdentifier;
81
82 /** @var Strings */
83 protected $strUtil;
84
85 /**
86 * Initialize is called in \Job
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 * Save Chosen Cloning Settings
107 * @return bool
108 * @throws \Exception
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 // Delete files index cache file
118 $this->filesIndexCache->delete();
119
120 // Generate Options
121 $this->options->root = str_replace(["\\", '/'], DIRECTORY_SEPARATOR, ABSPATH);
122 $this->options->current = null;
123 $this->options->currentClone = null;
124
125 // Clone ID -> timestamp (time at which this clone creation initiated)
126 $this->options->clone = preg_replace("#\W+#", '-', strtolower($this->sanitize->sanitizeString($_POST["cloneID"])));
127
128 // Clone Name -> Site name that user input
129 if (isset($_POST["cloneName"])) {
130 $this->options->cloneName = sanitize_text_field($_POST["cloneName"]);
131 }
132
133 // If it's empty or it's a clone Id, try setting it to a random human-friendly name
134 if (empty($this->options->cloneName) || $this->options->cloneName === $this->options->clone) {
135 $this->options->cloneName = $this->maybeGenerateFriendlyName();
136 }
137
138 // The slugified version of Clone Name (to use in directory creation)
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', // Important: Windows IIS configuration file. Must not be in the staging site!
160 '.wp-staging', // Determines if a site is a staging site
161 '.wp-staging-cloneable', // File that makes the staging site 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 // Job
183 $this->options->job = new \stdClass();
184 $this->loadLegacyExistingClones();
185
186 // Check if clone data already exists and use that one
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 // Clone does not exist but there are other clones in db
193 // Get data and increment it
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 // Included Tables / Prefixed Table - Excluded Tables
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 // Exclude File Size Rules
212 $this->options->excludeGlobRules = [];
213 if (!empty($_POST["excludeGlobRules"])) {
214 $this->options->excludeGlobRules = $this->sanitize->sanitizeExcludeRules($_POST["excludeGlobRules"]);
215 }
216
217 // Exclude Glob Rules
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 * @see /WPStaging/Framework/CloningProcess/ExcludedPlugins.php to exclude plugins
229 * Only add other directories here
230 */
231 $excludedDirectories = [
232 PathIdentifier::IDENTIFIER_WP_CONTENT . 'cache',
233 $this->pathIdentifier->transformPathToIdentifiable($pluginWpContentDir), // wp-content/wp-staging
234 PathIdentifier::IDENTIFIER_WP_CONTENT . WPSTG_PLUGIN_DOMAIN, // Extra caution if pluginWpContentDir changed later
235 ];
236
237 // Go Daddy related exclusions
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 // Add upload folder to list of excluded directories for push if symlink option is enabled
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 // Extra Directories
257 if (isset($_POST["extraDirectories"])) {
258 $this->options->extraDirectories = explode(ScanConst::DIRECTORIES_SEPARATOR, $this->sanitize->sanitizeString($_POST["extraDirectories"]));
259 }
260
261 // New Admin Account
262 $this->options->useNewAdminAccount = false;
263 $this->options->adminEmail = '';
264 $this->options->adminPassword = '';
265
266 // External Database
267 $this->options->databaseServer = 'localhost';
268 $this->options->databaseUser = '';
269 $this->options->databasePassword = '';
270 $this->options->databaseDatabase = '';
271 // isExternalDatabase() depends upon databaseUser and databasePassword,
272 // Make sure they are set before calling this.
273 $this->options->databasePrefix = $this->isExternalDatabase() ? $this->db->prefix : '';
274 $this->options->databaseSsl = false;
275
276 // Custom Hosts
277 $this->options->cloneDir = '';
278 $this->options->cloneHostname = '';
279
280 // Default options for FREE version
281 $this->options->isEmailsAllowed = true;
282 $this->options->isCronEnabled = true;
283 $this->options->isWooSchedulerEnabled = true;
284 $this->options->isEmailsReminderEnabled = false;
285 $this->options->isAutoUpdatePlugins = false;
286 $this->setAdvancedCloningOptions();
287
288 $this->options->destinationDir = $this->getDestinationDir();
289 $this->options->destinationHostname = $this->getDestinationHostname();
290
291 $this->options->homeHostname = $this->urls->getHomeUrlWithoutScheme();
292
293 // Process lock state
294 $this->options->isRunning = true;
295 $this->initializeLegacyStagingRun(Job::STAGING);
296
297 // id of the user creating the clone
298 $this->options->ownerId = get_current_user_id();
299 // Save Clone data
300 $this->saveClone();
301
302 if (!$this->saveOptions()) {
303 return false;
304 }
305
306 WPStaging::make(AnalyticsStagingCreate::class)->enqueueStartEvent($this->options->jobIdentifier, $this->options);
307 $this->errorMessage = "";
308 return true;
309 }
310
311 /**
312 * Save clone data initially
313 * @return void
314 */
315 private function saveClone()
316 {
317 // Save new clone data
318 $this->debugLog("Cloning: {$this->options->clone}'s clone job's data is not in database, generating data");
319
320 $this->options->existingClones[$this->options->clone] = [
321 "cloneName" => $this->options->cloneName,
322 "directoryName" => $this->options->cloneDirectoryName,
323 "path" => trailingslashit($this->options->destinationDir),
324 "url" => $this->getDestinationUrl(),
325 "number" => $this->options->cloneNumber,
326 "version" => WPStaging::getVersion(),
327 "status" => "unfinished or broken (?)",
328 "prefix" => $this->options->prefix,
329 "datetime" => time(),
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 * Get destination Hostname depending on whether WP has been installed in sub dir or not
361 * @return string
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 * Return target hostname
374 * @return string
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 * Return Hostname without scheme
387 * @param string $string
388 * @return string
389 */
390 private function getHostnameWithoutScheme(string $string): string
391 {
392 return preg_replace('#^https?://#', '', rtrim($string, '/'));
393 }
394
395 /**
396 * Get Destination Directory including staging subdirectory
397 * @return string
398 */
399 private function getDestinationDir(): string
400 {
401 // Throw fatal error
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 // custom destination has been set
407 if (!empty($this->options->cloneDir)) {
408 return trailingslashit($this->options->cloneDir);
409 }
410
411 // No custom destination so default path will be in a subfolder of root or inside wp-content
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 * Create a new staging prefix that does not exist in database
433 */
434 private function setStagingPrefix()
435 {
436 // Find a new prefix that does not already exist in database.
437 // Loop through up to 1000 different possible prefixes should be enough here;)
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 // Prefix does not exist. We can use it
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 * Start the cloning job
460 * @throws JobNotFoundException
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 // Call the job
502 return $this->{$methodName}();
503 }
504
505 /**
506 * @param object $response
507 * @param string $nextJob
508 * @return object
509 * @throws \Exception
510 */
511 private function handleJobResponse($response, string $nextJob)
512 {
513 // Job is not done
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 // Save options
524 $this->saveOptions();
525
526 return $response;
527 }
528
529 /**
530 * Copy data from staging site to temporary column to use it later
531 * @return object
532 * @throws \Exception
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 * Clone Database
544 * @return object
545 * @throws \Exception
546 */
547 public function jobDatabase()
548 {
549 $database = new Database();
550 return $this->handleJobResponse($database->start(), "SearchReplace");
551 }
552
553 /**
554 * Search & Replace
555 * @return object
556 * @throws \Exception
557 */
558 public function jobSearchReplace()
559 {
560 $searchReplace = new SearchReplace();
561 return $this->handleJobResponse($searchReplace->start(), "PreserveDataSecondStep");
562 }
563
564 /**
565 * Copy tmp data back to staging site
566 * @return object
567 * @throws \Exception
568 */
569 public function jobPreserveDataSecondStep()
570 {
571 $preserve = new PreserveDataSecondStep();
572 return $this->handleJobResponse($preserve->start(), 'directories');
573 }
574
575 /**
576 * Get All Files From Selected Directories Recursively Into a File
577 * @return object
578 * @throws \Exception
579 */
580 public function jobDirectories()
581 {
582 $directories = new Directories();
583 return $this->handleJobResponse($directories->start(), "files");
584 }
585
586 /**
587 * Copy Files
588 * @return object
589 * @throws \Exception
590 */
591 public function jobFiles()
592 {
593 $files = new Files();
594 return $this->handleJobResponse($files->start(), "data");
595 }
596
597 /**
598 * Replace Data
599 * @return object
600 * @throws \Exception
601 */
602 public function jobData()
603 {
604 $dataJob = $this->getDataJob();
605 return $this->handleJobResponse($dataJob->start(), "finish");
606 }
607
608 /**
609 * Save Clone Data
610 * @return object
611 * @throws \Exception
612 */
613 public function jobFinish()
614 {
615 // Re-generate the token when the Clone is complete.
616 // Todo: Consider adding a do_action() on jobFinish to hook here.
617 // Todo: Inject using DI
618 $accessToken = new AccessToken();
619 $accessToken->generateNewToken();
620
621 $finish = new Finish();
622 return $this->handleJobResponse($finish->start(), '');
623 }
624
625 /**
626 * @return Data
627 */
628 public function getDataJob(): Data
629 {
630 return new Data();
631 }
632
633 /**
634 * @return void
635 */
636 protected function setAdvancedCloningOptions()
637 {
638 // no-op
639 }
640
641 /**
642 * @return void
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 * @return string The generated friendly name or clone Id by default
677 * @throws WPStagingException
678 */
679 private function maybeGenerateFriendlyName(): string
680 {
681 // List of predefined names to choose from
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 // Randomly shuffle the list of names
709 shuffle($nameList);
710
711 // Get the list of staging sites
712 $stagingSites = $this->sitesHelper->tryGettingStagingSites();
713 foreach ($nameList as $name) {
714 // Sanitize the name to ensure it is safe for use
715 $name = sanitize_text_field($name);
716 $dirPath = ABSPATH . $name;
717 // Check if the directory exists
718 if (file_exists($dirPath)) {
719 continue;
720 }
721
722 // If the directory is free, then check the database
723 if (!$this->isStagingSiteNameExists($name, $stagingSites)) {
724 return $name;
725 }
726 }
727
728 // If all predefined names are taken, return a clone Id
729 return (string)$this->options->clone;
730 }
731
732 /**
733 * Check if the name already exists in the staging sites $stagingSites
734 * @param string $name
735 * @param array $stagingSites
736 * @return bool
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