PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 3.0.2
WP STAGING – WordPress Backups, Restore, Migration & Clone v3.0.2
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 5 years ago Exceptions 5 years ago Cancel.php 3 years ago CancelUpdate.php 4 years ago Cloning.php 3 years ago CloningProcess.php 3 years ago Data.php 3 years ago Database.php 3 years ago Delete.php 3 years ago Directories.php 3 years ago Files.php 3 years ago Finish.php 3 years ago Job.php 3 years ago JobExecutable.php 3 years ago Logs.php 3 years ago PreserveDataFirstStep.php 3 years ago PreserveDataSecondStep.php 3 years ago ProcessLock.php 3 years ago Scan.php 3 years ago SearchReplace.php 3 years ago TotalStepsAreNumberOfTables.php 5 years ago Updating.php 3 years ago Verify.php 3 years ago
Cloning.php
588 lines
1 <?php
2
3 namespace WPStaging\Backend\Modules\Jobs;
4
5 use Countable;
6 use WPStaging\Backend\Modules\Jobs\Exceptions\JobNotFoundException;
7 use WPStaging\Core\Utils\Helper;
8 use WPStaging\Core\WPStaging;
9 use WPStaging\Framework\Analytics\Actions\AnalyticsStagingCreate;
10 use WPStaging\Framework\Database\SelectedTables;
11 use WPStaging\Framework\Filesystem\PathIdentifier;
12 use WPStaging\Framework\Filesystem\Scanning\ScanConst;
13 use WPStaging\Framework\Security\AccessToken;
14 use WPStaging\Framework\Staging\Sites;
15 use WPStaging\Framework\Utils\Sanitize;
16 use WPStaging\Framework\Utils\SlashMode;
17 use WPStaging\Framework\Utils\WpDefaultDirectories;
18
19 /**
20 * Class Cloning
21 * @package WPStaging\Backend\Modules\Jobs
22 */
23 class Cloning extends Job
24 {
25
26 /**
27 * @var object
28 */
29 private $db;
30
31 /**
32 * @var WpDefaultDirectories
33 */
34 private $dirUtils;
35
36 /**
37 * @var Sites
38 */
39 private $sitesHelper;
40
41 /**
42 * @var string
43 */
44 private $errorMessage;
45
46 /**
47 * @var Sanitize
48 */
49 private $sanitize;
50
51 /**
52 * Initialize is called in \Job
53 */
54 public function initialize()
55 {
56 $this->db = WPStaging::getInstance()->get("wpdb");
57 $this->dirUtils = new WpDefaultDirectories();
58 $this->sitesHelper = new Sites();
59 $this->sanitize = WPStaging::make(Sanitize::class);
60 }
61
62 public function getErrorMessage()
63 {
64 return $this->errorMessage;
65 }
66
67 /**
68 * Save Chosen Cloning Settings
69 * @return bool
70 * @throws \Exception
71 */
72 public function save()
73 {
74 if (!isset($_POST) || !isset($_POST["cloneID"])) {
75 $this->errorMessage = __("clone ID missing", 'wp-staging');
76 return false;
77 }
78
79 // Delete files_to_copy.cache
80 $this->cache->delete("files_to_copy");
81
82 // Generate Options
83 // Clone ID -> timestamp (time at which this clone creation initiated)
84 $this->options->clone = preg_replace("#\W+#", '-', strtolower($this->sanitize->sanitizeString($_POST["cloneID"])));
85 // Clone Name -> Site name that user input, if user left it empty it will be Clone ID
86 $this->options->cloneName = isset($_POST["cloneName"]) ? sanitize_text_field($_POST["cloneName"]) : '';
87 // The slugified version of Clone Name (to use in directory creation)
88 $this->options->cloneDirectoryName = $this->sitesHelper->sanitizeDirectoryName($this->options->cloneName);
89 $result = $this->sitesHelper->isCloneExists($this->options->cloneDirectoryName);
90 if ($result !== false) {
91 $this->errorMessage = $result;
92 return false;
93 }
94
95 $this->options->cloneNumber = 1;
96 $this->options->prefix = $this->setStagingPrefix();
97 $this->options->includedDirectories = [];
98 $this->options->excludedDirectories = [];
99 $this->options->extraDirectories = [];
100 $this->options->excludedFiles = apply_filters('wpstg_clone_excluded_files', [
101 '.DS_Store',
102 '*.git',
103 '*.svn',
104 '*.tmp',
105 'desktop.ini',
106 '.gitignore',
107 '*.log',
108 'web.config', // Important: Windows IIS configuration file. Must not be in the staging site!
109 '.wp-staging', // Determines if a site is a staging site
110 '.wp-staging-cloneable', // File that makes the staging site cloneable.
111 ]);
112
113 $this->options->excludedFilesFullPath = apply_filters('wpstg.clone.excluded_files_full_path', [
114 '.htaccess',
115 PathIdentifier::IDENTIFIER_WP_CONTENT . 'db.php',
116 PathIdentifier::IDENTIFIER_WP_CONTENT . 'object-cache.php',
117 PathIdentifier::IDENTIFIER_WP_CONTENT . 'advanced-cache.php'
118 ]);
119
120 $this->options->currentStep = 0;
121
122 // Job
123 $this->options->job = new \stdClass();
124
125 // Check if clone data already exists and use that one
126 if (isset($this->options->existingClones[$this->options->clone])) {
127 $this->options->cloneNumber = $this->options->existingClones[$this->options->clone]->number;
128
129 $this->options->prefix = isset($this->options->existingClones[$this->options->clone]->prefix) ? $this->options->existingClones[$this->options->clone]->prefix : $this->setStagingPrefix();
130 }
131 // Clone does not exist but there are other clones in db
132 // Get data and increment it
133 elseif (!empty($this->options->existingClones)) {
134 $this->options->cloneNumber = count($this->options->existingClones) + 1;
135 }
136
137 $this->options->networkClone = false;
138 if ($this->isMultisiteAndPro() && is_main_site()) {
139 $this->options->networkClone = isset($_POST['networkClone']) && $this->sanitize->sanitizeBool($_POST['networkClone']);
140 }
141
142 // Included Tables / Prefixed Table - Excluded Tables
143 $includedTables = isset($_POST['includedTables']) ? $this->sanitize->sanitizeString($_POST['includedTables']) : '';
144 $excludedTables = isset($_POST['excludedTables']) ? $this->sanitize->sanitizeString($_POST['excludedTables']) : '';
145 $selectedTablesWithoutPrefix = isset($_POST['selectedTablesWithoutPrefix']) ? $this->sanitize->sanitizeString($_POST['selectedTablesWithoutPrefix']) : '';
146 $selectedTables = new SelectedTables($includedTables, $excludedTables, $selectedTablesWithoutPrefix);
147 $selectedTables->setAllTablesExcluded(empty($_POST['allTablesExcluded']) ? false : $this->sanitize->sanitizeBool($_POST['allTablesExcluded']));
148 $this->options->tables = $selectedTables->getSelectedTables($this->options->networkClone);
149
150 // Exclude File Size Rules
151 $this->options->excludeGlobRules = [];
152 if (!empty($_POST["excludeGlobRules"])) {
153 $this->options->excludeGlobRules = $this->sanitize->sanitizeExcludeRules($_POST["excludeGlobRules"]);
154 }
155
156 // Exclude Glob Rules
157 $this->options->excludeSizeRules = [];
158 if (!empty($_POST["excludeSizeRules"])) {
159 $this->options->excludeSizeRules = $this->sanitize->sanitizeExcludeRules($_POST["excludeSizeRules"]);
160 }
161
162 $this->options->uploadsSymlinked = isset($_POST['uploadsSymlinked']) && $this->sanitize->sanitizeBool($_POST['uploadsSymlinked']);
163
164 /**
165 * @see /WPStaging/Framework/CloningProcess/ExcludedPlugins.php to exclude plugins
166 * Only add other directories here
167 */
168 $excludedDirectories = [
169 PathIdentifier::IDENTIFIER_WP_CONTENT . 'cache',
170 ];
171
172 // Add upload folder to list of excluded directories for push if symlink option is enabled
173 if ($this->options->uploadsSymlinked) {
174 $excludedDirectories[] = PathIdentifier::IDENTIFIER_UPLOADS;
175 }
176
177 $excludedDirectoriesRequest = isset($_POST["excludedDirectories"]) ? $this->sanitize->sanitizeString($_POST["excludedDirectories"]) : '';
178 $excludedDirectoriesRequest = $this->dirUtils->getExcludedDirectories($excludedDirectoriesRequest);
179
180 $this->options->excludedDirectories = array_merge($excludedDirectories, $excludedDirectoriesRequest);
181
182 // Extra Directories
183 if (isset($_POST["extraDirectories"])) {
184 $this->options->extraDirectories = explode(ScanConst::DIRECTORIES_SEPARATOR, $this->sanitize->sanitizeString($_POST["extraDirectories"]));
185 }
186
187 $this->options->databaseServer = 'localhost';
188 if (!empty($_POST["databaseServer"])) {
189 $this->options->databaseServer = $this->sanitize->sanitizeString($_POST["databaseServer"]);
190 }
191
192 $this->options->databaseUser = '';
193 if (!empty($_POST["databaseUser"])) {
194 $this->options->databaseUser = $this->sanitize->sanitizeString($_POST["databaseUser"]);
195 }
196
197 $this->options->databasePassword = '';
198 if (!empty($_POST["databasePassword"])) {
199 $this->options->databasePassword = $this->sanitize->sanitizePassword($_POST["databasePassword"]);
200 }
201
202 $this->options->databaseDatabase = '';
203 if (!empty($_POST["databaseDatabase"])) {
204 $this->options->databaseDatabase = $this->sanitize->sanitizeString($_POST["databaseDatabase"]);
205 }
206
207 // isExternalDatabase() depends upon databaseUser and databasePassword,
208 // Make sure they are set before calling this.
209 $this->options->databasePrefix = $this->isExternalDatabase() ? $this->db->prefix : '';
210 if (!empty($_POST["databasePrefix"])) {
211 $this->options->databasePrefix = $this->maybeAppendUnderscorePrefix($this->sanitize->sanitizeString($_POST["databasePrefix"]));
212 }
213
214 $this->options->databaseSsl = false;
215 if (isset($_POST["databaseSsl"]) && 'true' === $this->sanitize->sanitizeString($_POST["databaseSsl"])) {
216 $this->options->databaseSsl = true;
217 }
218
219 $this->options->cloneDir = '';
220 if (!empty($_POST["cloneDir"])) {
221 $this->options->cloneDir = trailingslashit(wpstg_urldecode($this->sanitize->sanitizeString($_POST["cloneDir"])));
222 }
223
224 $this->options->cloneHostname = '';
225 if (!empty($_POST["cloneHostname"])) {
226 $this->options->cloneHostname = trim($this->sanitize->sanitizeString($_POST["cloneHostname"]));
227 }
228
229 // Make sure it is always enabled for free version
230 $this->options->emailsAllowed = true;
231 $this->options->cronDisabled = false;
232
233 if (defined('WPSTGPRO_VERSION')) {
234 $this->options->emailsAllowed = apply_filters(
235 'wpstg_cloning_email_allowed',
236 isset($_POST['emailsAllowed']) && $this->sanitize->sanitizeBool($_POST['emailsAllowed'])
237 );
238 $this->options->cronDisabled = !empty($_POST['cronDisabled']) ? $this->sanitize->sanitizeBool($_POST['cronDisabled']) : false;
239 }
240
241 $this->options->destinationHostname = $this->getDestinationHostname();
242 $this->options->destinationDir = $this->getDestinationDir();
243
244 $helper = new Helper();
245 $this->options->homeHostname = $helper->getHomeUrlWithoutScheme();
246
247 // Process lock state
248 $this->options->isRunning = true;
249
250 // id of the user creating the clone
251 $this->options->ownerId = get_current_user_id();
252
253 // Save Clone data
254 $this->saveClone();
255
256 WPStaging::make(AnalyticsStagingCreate::class)->enqueueStartEvent($this->options->jobIdentifier, $this->options);
257
258 $this->errorMessage = "";
259 return $this->saveOptions();
260 }
261
262 /**
263 * @return bool
264 */
265 private function isDestinationDatabaseSameAsLiveDatabase()
266 {
267 return $this->options->databaseServer === DB_HOST && $this->options->databaseDatabase === DB_NAME;
268 }
269
270 /**
271 * Save clone data initially
272 * @return void
273 */
274 private function saveClone()
275 {
276 // Save new clone data
277 $this->debugLog("Cloning: {$this->options->clone}'s clone job's data is not in database, generating data");
278
279 $this->options->existingClones[$this->options->clone] = [
280 "cloneName" => $this->options->cloneName,
281 "directoryName" => $this->options->cloneDirectoryName,
282 "path" => trailingslashit($this->options->destinationDir),
283 "url" => $this->getDestinationUrl(),
284 "number" => $this->options->cloneNumber,
285 "version" => WPStaging::getVersion(),
286 "status" => "unfinished or broken (?)",
287 "prefix" => $this->options->prefix,
288 "datetime" => time(),
289 "databaseUser" => $this->options->databaseUser,
290 "databasePassword" => $this->options->databasePassword,
291 "databaseDatabase" => $this->options->databaseDatabase,
292 "databaseServer" => $this->options->databaseServer,
293 "databasePrefix" => $this->options->databasePrefix,
294 "databaseSsl" => (bool)$this->options->databaseSsl,
295 "cronDisabled" => (bool)$this->options->cronDisabled,
296 "emailsAllowed" => (bool)$this->options->emailsAllowed,
297 "uploadsSymlinked" => (bool)$this->options->uploadsSymlinked,
298 "ownerId" => $this->options->ownerId,
299 "includedTables" => $this->options->tables,
300 "excludeSizeRules" => $this->options->excludeSizeRules,
301 "excludeGlobRules" => $this->options->excludeGlobRules,
302 "excludedDirectories" => $this->options->excludedDirectories,
303 "extraDirectories" => $this->options->extraDirectories,
304 "networkClone" => $this->isNetworkClone(),
305 ];
306
307 if ($this->sitesHelper->updateStagingSites($this->options->existingClones) === false) {
308 $this->log("Cloning: Failed to save {$this->options->clone}'s clone job data to database'");
309 }
310 }
311
312 /**
313 * Get destination Hostname depending on whether WP has been installed in sub dir or not
314 * @return string
315 */
316 private function getDestinationUrl()
317 {
318 if (!empty($this->options->cloneHostname)) {
319 return $this->options->cloneHostname;
320 }
321
322 return trailingslashit(get_site_url()) . $this->options->cloneDirectoryName;
323 }
324
325 /**
326 * Return target hostname
327 * @return string
328 */
329 private function getDestinationHostname()
330 {
331 if (empty($this->options->cloneHostname)) {
332 $helper = new Helper();
333 return $helper->getHomeUrlWithoutScheme();
334 }
335 return $this->getHostnameWithoutScheme($this->options->cloneHostname);
336 }
337
338 /**
339 * Return Hostname without scheme
340 * @param string $string
341 * @return string
342 */
343 private function getHostnameWithoutScheme($string)
344 {
345 return preg_replace('#^https?://#', '', rtrim($string, '/'));
346 }
347
348 /**
349 * Get Destination Directory including staging subdirectory
350 * @return string
351 */
352 private function getDestinationDir()
353 {
354 // Throw fatal error
355 if (!empty($this->options->cloneDir) & (trailingslashit($this->options->cloneDir) === trailingslashit(WPStaging::getWPpath()))) {
356 $this->returnException('Error: Target Directory must be different from the root of the production website.');
357 die();
358 }
359
360 // No custom clone dir so clone path will be in subfolder of root
361 if (empty($this->options->cloneDir)) {
362 $this->options->cloneDir = trailingslashit(WPStaging::getWPpath() . $this->options->cloneDirectoryName);
363 return $this->options->cloneDir;
364 }
365 return trailingslashit($this->options->cloneDir);
366 }
367
368 /**
369 * Make sure prefix ends with underscore
370 *
371 * @param string $string
372 * @return string
373 */
374 private function maybeAppendUnderscorePrefix($string)
375 {
376 $lastCharacter = substr($string, -1);
377 if ($lastCharacter === '_') {
378 return $string;
379 }
380 return $string . '_';
381 }
382
383 /**
384 * Create a new staging prefix that does not exist in database
385 */
386 private function setStagingPrefix()
387 {
388 // Find a new prefix that does not already exist in database.
389 // Loop through up to 1000 different possible prefixes should be enough here;)
390 for ($i = 0; $i <= 10000; $i++) {
391 $this->options->prefix = !empty($this->options->existingClones) && $this->options->existingClones instanceof Countable
392 ? 'wpstg' . (count($this->options->existingClones) + $i) . '_'
393 : 'wpstg' . $i . '_';
394
395 $sql = "SHOW TABLE STATUS LIKE '{$this->options->prefix}%'";
396 $tables = $this->db->get_results($sql);
397
398 // Prefix does not exist. We can use it
399 if (!$tables) {
400 return $this->options->prefix;
401 }
402 }
403
404 $message = sprintf("Fatal Error: Can not create staging prefix. '%s' already exists! Stopping for security reasons. Contact support@wp-staging.com", $this->options->prefix);
405 $this->returnException($message);
406 wp_die(esc_html($message));
407 }
408
409
410 /**
411 * Start the cloning job
412 * @throws JobNotFoundException
413 */
414 public function start()
415 {
416 if (!property_exists($this->options, 'currentJob') || $this->options->currentJob === null) {
417 $this->log("Cloning job finished");
418 return true;
419 }
420
421 $methodName = "job" . ucwords($this->options->currentJob);
422
423 if (!method_exists($this, $methodName)) {
424 $this->log("Can't execute job; Job's method $methodName is not found");
425 throw new JobNotFoundException($methodName);
426 }
427
428 if ($this->options->databasePrefix === $this->db->prefix && $this->isDestinationDatabaseSameAsLiveDatabase()) {
429 $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.');
430 }
431
432 // Call the job
433 return $this->{$methodName}();
434 }
435
436 /**
437 * @param object $response
438 * @param string $nextJob
439 * @return object
440 * @throws \Exception
441 */
442 private function handleJobResponse($response, $nextJob)
443 {
444 // Job is not done
445 if ($response->status !== true) {
446 return $response;
447 }
448
449 $this->options->job = new \stdClass();
450 $this->options->currentJob = $nextJob;
451 $this->options->currentStep = 0;
452 $this->options->totalSteps = 0;
453
454 // Save options
455 $this->saveOptions();
456
457 return $response;
458 }
459
460 /**
461 * Copy data from staging site to temporary column to use it later
462 * @return object
463 * @throws \Exception
464 */
465 public function jobPreserveDataFirstStep()
466 {
467 $this->writeJobSpecificLogStartHeader();
468
469 $preserve = new PreserveDataFirstStep();
470 return $this->handleJobResponse($preserve->start(), 'database');
471 }
472
473 /**
474 * Clone Database
475 * @return object
476 * @throws \Exception
477 */
478 public function jobDatabase()
479 {
480 $database = new Database();
481 return $this->handleJobResponse($database->start(), "SearchReplace");
482 }
483
484 /**
485 * Search & Replace
486 * @return object
487 * @throws \Exception
488 */
489 public function jobSearchReplace()
490 {
491 $searchReplace = new SearchReplace();
492 return $this->handleJobResponse($searchReplace->start(), "PreserveDataSecondStep");
493 }
494
495 /**
496 * Copy tmp data back to staging site
497 * @return object
498 * @throws \Exception
499 */
500 public function jobPreserveDataSecondStep()
501 {
502 $preserve = new PreserveDataSecondStep();
503 return $this->handleJobResponse($preserve->start(), 'directories');
504 }
505
506 /**
507 * Get All Files From Selected Directories Recursively Into a File
508 * @return object
509 * @throws \Exception
510 */
511 public function jobDirectories()
512 {
513 $directories = new Directories();
514 return $this->handleJobResponse($directories->start(), "files");
515 }
516
517 /**
518 * Copy Files
519 * @return object
520 * @throws \Exception
521 */
522 public function jobFiles()
523 {
524 $files = new Files();
525 return $this->handleJobResponse($files->start(), "data");
526 }
527
528
529 /**
530 * Replace Data
531 * @return object
532 * @throws \Exception
533 */
534 public function jobData()
535 {
536 return $this->handleJobResponse((new Data())->start(), "finish");
537 }
538
539 /**
540 * Save Clone Data
541 * @return object
542 * @throws \Exception
543 */
544 public function jobFinish()
545 {
546 // Re-generate the token when the Clone is complete.
547 // Todo: Consider adding a do_action() on jobFinish to hook here.
548 // Todo: Inject using DI
549 $accessToken = new AccessToken();
550 $accessToken->generateNewToken();
551
552 $finish = new Finish();
553 return $this->handleJobResponse($finish->start(), '');
554 }
555
556 /**
557 * @return void
558 */
559 private function writeJobSpecificLogStartHeader()
560 {
561
562 $jobName = empty($this->options->mainJob) ? 'Unknown' : $this->options->mainJob;
563
564 switch($jobName) {
565 case 'updating':
566 $jobName = 'Update';
567 break;
568 case 'resetting':
569 $jobName = 'Reset';
570 break;
571 case 'cloning':
572 $jobName = 'Cloning';
573 break;
574 default:
575 $jobName = 'Unknown';
576 break;
577 }
578
579 $this->log('#################### Start ' . $jobName . ' Job ####################', 'INFO');
580 if ($jobName !== 'Cloning' && !empty($this->options->clone)) {
581 $this->logger->info(esc_html('Staging Site ID: ' . $this->options->clone));
582 $this->logger->info(esc_html('Staging Site: ' . $this->options->cloneName));
583 }
584
585 $this->logger->writeLogHeader();
586 }
587 }
588