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 / Scan.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
Scan.php
803 lines
1 <?php
2
3 namespace WPStaging\Backend\Modules\Jobs;
4
5 use DirectoryIterator;
6 use Exception;
7 use RuntimeException;
8 use UnexpectedValueException;
9 use WPStaging\Backend\Optimizer\Optimizer;
10 use WPStaging\Core\WPStaging;
11 use WPStaging\Framework\Adapter\Directory;
12 use WPStaging\Framework\Filesystem\DiskWriteCheck;
13 use WPStaging\Framework\Filesystem\Scanning\ScanConst;
14 use WPStaging\Staging\Sites;
15 use WPStaging\Framework\Utils\Sanitize;
16 use WPStaging\Framework\Utils\Strings;
17 use WPStaging\Framework\Utils\WpDefaultDirectories;
18 use WPStaging\Framework\Job\Exception\DiskNotWritableException;
19 use WPStaging\Framework\Filesystem\PathChecker;
20 use WPStaging\Framework\SiteInfo;
21 use WPStaging\Framework\TemplateEngine\TemplateEngine;
22 use WPStaging\Framework\Filesystem\PathIdentifier;
23
24 /**
25 * Builds the directory tree and related metadata for clone jobs before the staging run starts.
26 */
27 class Scan extends Job
28 {
29 /**
30 * CSS class name to use for WordPress core directories like wp-content, wp-admin, wp-includes
31 * This doesn't contain class selector prefix
32 *
33 * @var string
34 */
35 const WP_CORE_DIR = "wpstg-wp-core-dir";
36
37 /**
38 * CSS class name to use for WordPress non core directories
39 * This doesn't contain class selector prefix
40 *
41 * @var string
42 */
43 const WP_NON_CORE_DIR = "wpstg-wp-non-core-dir";
44
45 /** @var array */
46 private $directories = [];
47
48 /** @var string|null */
49 private $directoryToScanOnly;
50
51 /**
52 * @var string Path to gif loader for directory loading
53 */
54 private $gifLoaderPath;
55
56 /**
57 * @var Strings
58 */
59 private $strUtils;
60
61 /**
62 * @var Directory
63 */
64 protected $dirAdapter;
65
66 /**
67 * @var DiskWriteCheck
68 */
69 private $diskWriteCheck;
70
71 /**
72 * @var Sanitize
73 */
74 private $sanitize;
75
76 /**
77 * @var string Path to the info icon
78 */
79 private $infoIconPath;
80
81 /**
82 * @var string
83 */
84 private $basePath;
85
86 /**
87 * @var string
88 */
89 private $pathIdentifier;
90
91 /**
92 * @var TemplateEngine
93 */
94 private $templateEngine;
95
96 /** @var PathIdentifier */
97 private $pathAdapter;
98
99 /** @var PathChecker */
100 private $pathChecker;
101
102 /** @var string */
103 protected $absPath = ABSPATH;
104
105 /** @var string */
106 protected $wpContentPath = WP_CONTENT_DIR;
107
108 /** @var bool */
109 private $isUploadsSymlinked;
110
111 /**
112 * Job constructor.
113 * @throws Exception
114 */
115 public function __construct($directoryToScanOnly = null)
116 {
117 // Accept both the absolute path or relative path with respect to wp root
118 // Santized the path to make comparing works for windows platform too.
119 $this->directoryToScanOnly = null;
120 if ($directoryToScanOnly !== null) {
121 $this->directoryToScanOnly = $directoryToScanOnly;
122 }
123
124 // TODO: inject using DI when available
125 $this->strUtils = new Strings();
126 $this->pathAdapter = WPStaging::make(PathIdentifier::class);
127 $this->pathChecker = WPStaging::make(PathChecker::class);
128 $this->dirAdapter = WPStaging::make(Directory::class);
129 $this->diskWriteCheck = WPStaging::make(DiskWriteCheck::class);
130 $this->sanitize = WPStaging::make(Sanitize::class);
131 $this->templateEngine = WPStaging::make(TemplateEngine::class);
132 parent::__construct();
133 }
134
135 /**
136 * @param string $gifLoaderPath
137 */
138 public function setGifLoaderPath(string $gifLoaderPath)
139 {
140 $this->gifLoaderPath = $gifLoaderPath;
141 }
142
143 /**
144 * @param string $infoIconPath
145 */
146 public function setInfoIcon(string $infoIconPath)
147 {
148 $this->infoIconPath = $infoIconPath;
149 }
150
151 /**
152 * Return the path of info icon
153 *
154 * @return string
155 */
156 public function getInfoIcon(): string
157 {
158 return $this->infoIconPath;
159 }
160
161 /**
162 * @param string $directoryToScanOnly
163 */
164 public function setDirectoryToScanOnly(string $directoryToScanOnly)
165 {
166 $this->directoryToScanOnly = $directoryToScanOnly;
167 }
168
169 /**
170 * @param string $basePath
171 * @todo add typed property `string` and ensure this value is never null
172 */
173 public function setBasePath($basePath)
174 {
175 $this->basePath = rtrim(wp_normalize_path($basePath), '/');
176 }
177
178 /** @return string */
179 public function getBasePath(): string
180 {
181 return $this->basePath;
182 }
183
184 /** @param string $pathIdentifier */
185 public function setPathIdentifier(string $pathIdentifier)
186 {
187 $this->pathIdentifier = $pathIdentifier;
188 }
189
190 /** @return string */
191 public function getPathIdentifier(): string
192 {
193 return $this->pathIdentifier;
194 }
195
196 /**
197 * Upon class initialization
198 */
199 public function initialize()
200 {
201 $this->options->existingClones = get_option(Sites::STAGING_SITES_OPTION, []);
202 $this->options->existingClones = is_array($this->options->existingClones) ? $this->options->existingClones : [];
203
204 $this->directories = [];
205 if (!empty($this->directoryToScanOnly)) {
206 return;
207 }
208
209 $this->getTables();
210
211 $this->setBasePath($this->absPath);
212 $this->setPathIdentifier(PathIdentifier::IDENTIFIER_ABSPATH);
213 $this->getDirectories($this->absPath);
214
215 // If wp-content is outside ABSPATH, then scan it too
216 if ($this->isWpContentOutsideAbspath()) {
217 $this->setBasePath($this->wpContentPath);
218 $this->setPathIdentifier(PathIdentifier::IDENTIFIER_WP_CONTENT);
219 $this->getDirectories(dirname($this->wpContentPath));
220 }
221
222 $this->installOptimizer();
223 }
224
225 /**
226 * Start Module
227 * @return $this|object
228 * @throws Exception
229 */
230 public function start()
231 {
232 // Basic Options
233 $this->options->root = str_replace(["\\", '/'], DIRECTORY_SEPARATOR, ABSPATH);
234 $this->options->current = null;
235 $this->options->currentClone = $this->getCurrentClone();
236
237 if ($this->options->currentClone !== null) {
238 // Make sure no warning is shown when updating/resetting an old clone having no exclude rules options
239 $this->options->currentClone['excludeSizeRules'] = $this->options->currentClone['excludeSizeRules'] ?? [];
240 $this->options->currentClone['excludeGlobRules'] = $this->options->currentClone['excludeGlobRules'] ?? [];
241 // Make sure no warning is shown when updating/resetting an old clone having no admin account data
242 $this->options->currentClone['useNewAdminAccount'] = $this->options->currentClone['useNewAdminAccount'] ?? false;
243 $this->options->currentClone['adminEmail'] = $this->options->currentClone['adminEmail'] ?? '';
244 $this->options->currentClone['adminPassword'] = $this->options->currentClone['adminPassword'] ?? '';
245 // Make sure no warning is shown when updating/resetting an old clone without databaseSsl, uploadsSymlinked, isEmailsAllowed and networkClone options
246 $this->options->currentClone['isEmailsAllowed'] = $this->options->currentClone['isEmailsAllowed'] ?? true;
247 $this->options->currentClone['databaseSsl'] = $this->options->currentClone['databaseSsl'] ?? false;
248 $this->options->currentClone['uploadsSymlinked'] = $this->options->currentClone['uploadsSymlinked'] ?? false;
249 $this->options->currentClone['networkClone'] = $this->options->currentClone['networkClone'] ?? false;
250 $this->options->currentClone['isWooSchedulerEnabled'] = empty($this->options->currentClone['isWooSchedulerEnabled']) ? false : true;
251 $this->options->currentClone['isEmailsReminderEnabled'] = empty($this->options->currentClone['isEmailsReminderEnabled']) ? false : true;
252 $this->options->currentClone['isAutoUpdatePlugins'] = empty($this->options->currentClone['isAutoUpdatePlugins']) ? false : true;
253 }
254
255 // Tables
256 $this->options->clonedTables = [];
257
258 // Files
259 $this->options->totalFiles = 0;
260 $this->options->totalFileSize = 0;
261 $this->options->copiedFiles = 0;
262
263
264 // Directories
265 $this->options->includedDirectories = [];
266 $this->options->includedExtraDirectories = [];
267 $this->options->excludedDirectories = [];
268 $this->options->extraDirectories = [];
269 $this->options->scannedDirectories = [];
270
271 // Job
272 $this->options->currentJob = "PreserveDataFirstStep";
273 $this->options->currentStep = 0;
274 $this->options->totalSteps = 0;
275
276 // Define mainJob to differentiate between cloning, updating and pushing
277 $this->options->mainJob = Job::STAGING;
278 $job = '';
279 if (isset($_POST["job"])) {
280 $job = $this->sanitize->sanitizeString($_POST['job']);
281 }
282
283 if ($this->options->current !== null && $job === 'resetting') {
284 $this->options->mainJob = Job::RESET;
285 } elseif ($this->options->current !== null) {
286 $this->options->mainJob = Job::UPDATE;
287 }
288
289 // Delete previous cached files
290 $this->cloneOptionCache->delete();
291 $this->filesIndexCache->delete();
292
293 $this->saveOptions();
294
295 return $this;
296 }
297
298 /**
299 * Make sure the Optimizer mu plugin is installed before cloning or pushing
300 */
301 private function installOptimizer()
302 {
303 $optimizer = new Optimizer();
304 $optimizer->installOptimizer();
305 }
306
307 /**
308 * @param bool|null $parentChecked Is parent folder selected
309 * @param bool $forceDefault Default false. Set it to true,
310 * when default button on ui is clicked,
311 * to ignore previous selected option for UPDATE and RESET process.
312 * @param null|array $directories to list
313 *
314 * @return string
315 */
316 public function directoryListing($parentChecked = null, $forceDefault = false, $directories = null): string
317 {
318 if ($directories === null) {
319 $directories = $this->directories;
320 }
321
322 uksort($directories, 'strcasecmp');
323
324 $excludedDirectories = [];
325 $extraDirectories = [];
326
327 if ($this->isUpdateOrResetJob()) {
328 $currentClone = json_decode(json_encode($this->options->currentClone));
329 $extraDirectories = isset($currentClone->extraDirectories) ? $currentClone->extraDirectories : [];
330 $excludedDirectories = isset($currentClone->excludedDirectories) ? array_map(function ($directory) {
331 // Exception is thrown when directory doesn't have identifier, so we will return directory as it is
332 try {
333 return $this->pathAdapter->transformIdentifiableToPath($directory);
334 } catch (UnexpectedValueException $ex) {
335 return $directory;
336 }
337 }, $currentClone->excludedDirectories) : [];
338 }
339
340 $output = '';
341 foreach ($directories as $dirName => $directory) {
342 // Not a directory, possibly a symlink, therefore we will skip it
343 if (!is_array($directory) || basename($dirName) === "\\") {
344 continue;
345 }
346
347 // Need to preserve keys so no array_shift()
348 $data = reset($directory);
349 unset($directory[key($directory)]);
350
351 $output .= $this->getDirectoryHtml($data['dirName'], $data, $excludedDirectories, $extraDirectories, $parentChecked, $forceDefault);
352 }
353
354 return $output;
355 }
356
357 /**
358 * Checks if there is enough free disk space to create staging site according to selected directories
359 * Returns null when can't run disk_free_space function one way or another
360 * @param string $excludedDirectories
361 * @param string $extraDirectories
362 *
363 * @return bool|null
364 */
365 public function hasFreeDiskSpace(string $excludedDirectories, string $extraDirectories)
366 {
367 $dirUtils = new WpDefaultDirectories();
368 $selectedDirectories = $dirUtils->getWpCoreDirectories();
369 $excludedDirectories = $dirUtils->getExcludedDirectories($excludedDirectories);
370
371 if ($this->isUploadsSymlinked) {
372 $uploadDirectory = rtrim(str_replace($this->absPath, PathIdentifier::IDENTIFIER_ABSPATH, $this->dirAdapter->getMainSiteUploadsDirectory()), '/');
373 $excludedDirectories[] = $uploadDirectory;
374 }
375
376 $size = 0;
377 // Scan WP Root path for size (only files)
378 $size += $this->getDirectorySizeExcludingSubdirs($this->absPath);
379 // Scan selected directories for size (wp-core)
380 foreach ($selectedDirectories as $directory) {
381 if ($this->isPathInDirectories($directory, $excludedDirectories)) {
382 continue;
383 }
384
385 $size += $this->getDirectorySizeInclSubdirs($directory, $excludedDirectories);
386 }
387
388 if (!empty($extraDirectories) && $extraDirectories !== '') {
389 $extraDirectories = wpstg_urldecode(explode(ScanConst::DIRECTORIES_SEPARATOR, $extraDirectories));
390 foreach ($extraDirectories as $directory) {
391 $size += $this->getDirectorySizeInclSubdirs($this->absPath . $directory, $excludedDirectories);
392 }
393 }
394
395 $errorMessage = null;
396 try {
397 $this->diskWriteCheck->checkPathCanStoreEnoughBytes($this->absPath, $size);
398 } catch (RuntimeException $ex) {
399 $errorMessage = $ex->getMessage();
400 } catch (DiskNotWritableException $ex) {
401 $errorMessage = $ex->getMessage();
402 }
403
404 $data = [
405 'requiredSpace' => $this->utilsMath->formatSize($size),
406 'errorMessage' => $errorMessage,
407 ];
408
409 echo json_encode($data);
410 die();
411 }
412
413 /**
414 * Get Database Tables
415 */
416 protected function getTables()
417 {
418 $db = WPStaging::getInstance()->get("wpdb");
419 $dbPrefix = WPStaging::getTablePrefix();
420
421 $sql = "SHOW TABLE STATUS";
422
423 $tables = $db->get_results($sql);
424
425 $currentTables = [];
426
427 $currentClone = $this->getCurrentClone();
428 $networkClone = is_multisite() && is_main_site() && is_array($currentClone) && (array_key_exists('networkClone', $currentClone) ? $this->sanitize->sanitizeBool($currentClone['networkClone']) : false);
429
430 // Reset excluded Tables than loop through all tables
431 $this->options->excludedTables = [];
432 foreach ($tables as $table) {
433 // Create array of unchecked tables
434 // On the main website of a multisite installation, do not select network site tables beginning with wp_1_, wp_2_ etc.
435 // (On network sites, the correct tables are selected anyway)
436 if (
437 ( ! empty($dbPrefix) && strpos($table->Name, $dbPrefix) !== 0)
438 || (is_multisite() && is_main_site() && !$networkClone && preg_match('/^' . $dbPrefix . '\d+_/', $table->Name))
439 ) {
440 $this->options->excludedTables[] = $table->Name;
441 }
442
443 if ($table->Comment !== "VIEW") {
444 $currentTables[] = [
445 "name" => $table->Name,
446 "size" => ($table->Data_length + $table->Index_length),
447 ];
448 }
449 }
450
451 $this->options->tables = json_decode(json_encode($currentTables));
452 }
453
454 /**
455 * Get directories and main meta data about given directory path
456 * @param string $dirPath - Optional - Default ABSPATH
457 * @param bool $shouldReturn - Optional - Default false
458 *
459 * @return void|array Depend upon value of $shouldReturn
460 */
461 public function getDirectories(string $dirPath = ABSPATH, bool $shouldReturn = false)
462 {
463 if (!is_dir($dirPath)) {
464 return;
465 }
466
467 try {
468 $directories = new DirectoryIterator($dirPath);
469 } catch (UnexpectedValueException $ex) {
470 $errorMessage = $ex->getMessage();
471 if ($ex->getCode() === 5) {
472 $errorMessage = esc_html__('Access Denied: No read permission to scan the root directory for cloning. Alternatively you can try the WP STAGING backup feature!', 'wp-staging');
473 }
474
475 echo json_encode([
476 'success' => false,
477 'type' => '',
478 // TODO: Create a Swal Response Class and Js library to handle that response or, Implement own Swal alternative
479 'swalOptions' => [
480 'title' => esc_html__('Error!', 'wp-staging'),
481 'html' => $errorMessage,
482 'cancelButtonText' => esc_html__('Ok', 'wp-staging'),
483 'showCancelButton' => true,
484 'showConfirmButton' => false,
485 ],
486 ]);
487
488 exit();
489 }
490
491 $result = [];
492
493 foreach ($directories as $directory) {
494 if ($directory->isDot() || $directory->isFile()) {
495 continue;
496 }
497
498 $fullPath = $this->resolveDirectoryPath($directory);
499 if (empty($fullPath) || !is_dir($fullPath)) {
500 continue;
501 }
502
503 // If filename is int, then it is treated as a numeric index in key and start with 0
504 $result[$directory->getFilename()]['metaData'] = [
505 'dirName' => $directory->getFilename(),
506 "path" => $fullPath,
507 "basePath" => $this->getBasePath(),
508 "prefix" => $this->getPathIdentifier(),
509 "isLink" => is_link($directory->getPathname()),
510 ];
511 }
512
513 if ($shouldReturn) {
514 return $result;
515 }
516
517 $this->directories = array_merge($this->directories, $result);
518 }
519
520 /**
521 * Get Path from $directory
522 * @param DirectoryIterator $directory
523 * @return bool|string
524 */
525 protected function getPath($directory)
526 {
527 $basePath = $this->getBasePath();
528 $realPath = WPStaging::make('WPSTG_ALLOW_VFS') === true && strpos($directory->getPathname(), 'vfs://') === 0 ? $directory->getPathname() : $directory->getRealPath();
529 $realPath = wp_normalize_path($realPath);
530
531 /*
532 * Do not follow root path like src/web/..
533 * This must be done before \SplFileInfo->isDir() is used!
534 * Prevents open base dir restriction fatal errors
535 */
536 if (strpos($realPath, $basePath) !== 0) {
537 return false;
538 }
539
540 $path = str_replace($basePath, '', $realPath);
541 // Using strpos() for symbolic links as they could create nasty stuff in nix stuff for directory structures
542 if (!$directory->isDir() || (strlen($path) < 1 && $this->pathIdentifier !== PathIdentifier::IDENTIFIER_WP_CONTENT)) {
543 return false;
544 }
545
546 return $path;
547 }
548
549 /**
550 * @param string $dirName
551 * @param array $dirInfo contains information about the directory
552 * @param array $excludedDirectories
553 * @param array $extraDirectories
554 * @param bool $parentChecked
555 * @param bool $forceDefault
556 * @return string
557 */
558 protected function getDirectoryHtml($dirName, $dirInfo, $excludedDirectories, $extraDirectories, $parentChecked = false, $forceDefault = false)
559 {
560 $data = $dirInfo;
561 $dataPath = isset($data["path"]) ? $data["path"] : '';
562 $path = wp_normalize_path($dataPath);
563 $basePath = isset($data["basePath"]) ? $data["basePath"] : wp_normalize_path($this->absPath);
564 $prefix = isset($data["prefix"]) ? $data["prefix"] : PathIdentifier::IDENTIFIER_ABSPATH;
565 $relPath = str_replace($basePath, '', $path);
566 $relPath = ltrim($relPath, '/');
567
568 // Check if directory name or directory path is not WP core folder
569 $isNotWPCoreDir = $this->isNonWpCoreDirectory($dirName, $path);
570
571 $class = $isNotWPCoreDir ? self::WP_NON_CORE_DIR : self::WP_CORE_DIR;
572 $dirType = 'other';
573
574 if ($this->strUtils->startsWith($path, $this->dirAdapter->getPluginsDirectory()) !== false) {
575 $pluginPath = $this->strUtils->strReplaceFirst($this->dirAdapter->getPluginsDirectory(), '', $path);
576 $dirType = strpos($pluginPath, '/') === false ? 'plugin' : 'other';
577 } elseif ($this->strUtils->startsWith($path, $this->dirAdapter->getActiveThemeParentDirectory()) !== false) {
578 $themePath = $this->strUtils->strReplaceFirst($this->dirAdapter->getActiveThemeParentDirectory(), '', $path);
579 $dirType = strpos($themePath, '/') === false ? 'theme' : 'other';
580 }
581
582 $isScanned = 'false';
583 if (
584 trailingslashit($path) === $this->dirAdapter->getWpContentDirectory()
585 || trailingslashit($path) === $this->dirAdapter->getPluginsDirectory()
586 || trailingslashit($path) === $this->dirAdapter->getActiveThemeParentDirectory()
587 ) {
588 $isScanned = 'true';
589 }
590
591 // Make wp-includes and wp-admin directory items not expandable
592 $isNavigatable = 'true';
593 if ($this->strUtils->startsWith($path, $basePath . "/wp-admin") !== false || $this->strUtils->startsWith($path, $basePath . "/wp-includes") !== false) {
594 $isNavigatable = 'false';
595 }
596
597 // Decide if item checkbox is active or not
598 $shouldBeChecked = $parentChecked !== null ? $parentChecked : !$isNotWPCoreDir;
599 if (!$forceDefault && $this->isUpdateOrResetJob() && (!$this->isPathInDirectories($path, $excludedDirectories, $basePath))) {
600 $shouldBeChecked = true;
601 } elseif (!$forceDefault && $this->isUpdateOrResetJob()) {
602 $shouldBeChecked = false;
603 }
604
605 if (!$forceDefault && $this->isUpdateOrResetJob() && $class === self::WP_NON_CORE_DIR && !$this->isPathInDirectories($path, $extraDirectories)) {
606 $shouldBeChecked = false;
607 }
608
609 $isDisabledDir = $dirName === 'wp-admin' || $dirName === 'wp-includes';
610
611 $isDisabled = false;
612 if (strpos($dataPath, 'wp-content/' . Directory::STAGING_SITE_DIRECTORY) !== false) {
613 $isDisabled = true;
614 $shouldBeChecked = false;
615 }
616
617 $isLink = false;
618 if (strpos(trailingslashit($basePath) . $dirName, 'wp-content') !== false && is_link(trailingslashit($basePath) . $dirName)) {
619 $isDisabled = true;
620 $isNavigatable = 'false';
621 $shouldBeChecked = true;
622 $isLink = true;
623 $relPath = 'wp-content';
624 }
625
626 return $this->templateEngine->render('clone/ajax/directory-navigation.php', [
627 'scan' => $this,
628 'prefix' => $prefix,
629 'relPath' => $relPath,
630 'class' => $class,
631 'dirType' => $dirType,
632 'isScanned' => $isScanned,
633 'isNavigatable' => $isNavigatable,
634 'shouldBeChecked' => $shouldBeChecked,
635 'parentChecked' => $parentChecked,
636 'directoryDisabled' => $isNotWPCoreDir || $isDisabledDir,
637 'isDisabled' => $isDisabled,
638 'dirName' => $dirName,
639 'gifLoaderPath' => $this->gifLoaderPath,
640 'isDebugMode' => false,
641 'dataPath' => $dataPath,
642 'basePath' => $basePath,
643 'forceDefault' => $forceDefault,
644 'dirPath' => $path,
645 'isLink' => $isLink,
646 ]);
647 }
648
649 /**
650 * Get total size of a directory including all its subdirectories
651 * @param string $dir
652 * @param array $excludedDirectories
653 * @return int
654 */
655 protected function getDirectorySizeInclSubdirs($dir, $excludedDirectories)
656 {
657 $size = 0;
658 foreach (glob(rtrim($dir, '/') . '/*', GLOB_NOSORT) as $each) {
659 if (is_file($each)) {
660 $size += filesize($each);
661 continue;
662 }
663
664 if ($this->isPathInDirectories($each, $excludedDirectories)) {
665 continue;
666 }
667
668 $size += $this->getDirectorySizeInclSubdirs($each, $excludedDirectories);
669 }
670
671 return $size;
672 }
673
674 /**
675 * Get total size of a directory excluding all its subdirectories
676 * @param string $dir
677 * @return int
678 */
679 protected function getDirectorySizeExcludingSubdirs($dir)
680 {
681 $size = 0;
682 foreach (glob(rtrim($dir, '/') . '/*', GLOB_NOSORT) as $each) {
683 $size += is_file($each) ? filesize($each) : 0;
684 }
685
686 return $size;
687 }
688
689 /**
690 * Is the path present is given list of directories
691 * @param string $path
692 * @param array $directories List of directories relative to ABSPATH with leading slash
693 * @param ?string $basePath
694 *
695 * @return bool
696 */
697 protected function isPathInDirectories(string $path, array $directories, $basePath = null): bool
698 {
699 return $this->pathChecker->isPathInPathsList($path, $directories, true, $basePath);
700 }
701
702 /**
703 * Get clone from $_POST['clone'] and set it as current clone
704 * If clone is not found, then set current clone to null
705 *
706 * @return array|null
707 */
708 protected function getCurrentClone()
709 {
710 $cloneID = isset($_POST["clone"]) ? $this->sanitize->sanitizeString($_POST['clone']) : '';
711
712 if (array_key_exists($cloneID, $this->options->existingClones)) {
713 $this->options->current = $cloneID;
714 return $this->options->existingClones[$this->options->current];
715 }
716
717 return null;
718 }
719
720 /**
721 * @return bool
722 */
723 protected function isWpContentOutsideAbspath()
724 {
725 /** @var SiteInfo $siteInfo */
726 $siteInfo = WPStaging::make(SiteInfo::class);
727 return $siteInfo->isWpContentOutsideAbspath();
728 }
729
730 /**
731 * Check if directory name or directory path is not WP core folder
732 *
733 * @param string $dirname
734 * @param string $path
735 * @return bool
736 */
737 protected function isNonWpCoreDirectory($dirname, $path)
738 {
739 $coreDirectories = [
740 'wp-admin',
741 'wp-content',
742 'wp-includes',
743 ];
744
745 if (in_array($dirname, $coreDirectories)) {
746 return false;
747 }
748
749 $wpDirectories = [
750 $this->dirAdapter->getWpContentDirectory(),
751 $this->dirAdapter->getPluginsDirectory(),
752 $this->dirAdapter->getActiveThemeParentDirectory(),
753 $this->dirAdapter->getUploadsDirectory(),
754 $this->dirAdapter->getMuPluginsDirectory(),
755 ];
756
757 foreach ($wpDirectories as $wpDirectory) {
758 if (strpos(trailingslashit($path), $wpDirectory) !== false) {
759 return false;
760 }
761 }
762
763 return true;
764 }
765
766 /**
767 * Resolve the full path for a directory, handling symlinks if present
768 * @param DirectoryIterator $directory
769 * @return string
770 */
771 private function resolveDirectoryPath(DirectoryIterator $directory): string
772 {
773 if (!is_link($directory->getPathname())) {
774 $path = $this->getPath($directory);
775 if ($path === false) {
776 return '';
777 }
778
779 return trailingslashit($this->getBasePath()) . ltrim($path, '/');
780 }
781
782 $targetPath = readlink($directory->getPathname());
783 if ($targetPath === false) {
784 return '';
785 }
786
787 if (!path_is_absolute($targetPath)) {
788 $targetPath = dirname($directory->getPathname()) . '/' . $targetPath;
789 }
790
791 return wp_normalize_path(realpath($targetPath));
792 }
793
794 /**
795 * @param bool $isUploadsSymlinked
796 * @return void
797 */
798 public function setIsUploadsSymlinked(bool $isUploadsSymlinked)
799 {
800 $this->isUploadsSymlinked = $isUploadsSymlinked;
801 }
802 }
803