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 / Staging / Service / DirectoryScanner.php
wp-staging / Staging / Service Last commit date
Database 1 day ago AbstractStagingSetup.php 1 day ago DirectoryScanner.php 1 day ago FileCopier.php 1 day ago LegacyOptionsCache.php 1 day ago StagingEngine.php 1 day ago StagingSetup.php 1 day ago TableScanner.php 1 day ago
DirectoryScanner.php
629 lines
1 <?php
2
3 namespace WPStaging\Staging\Service;
4
5 use DirectoryIterator;
6 use RuntimeException;
7 use Throwable;
8 use UnexpectedValueException;
9 use WPStaging\Framework\Adapter\Directory;
10 use WPStaging\Framework\Assets\Assets;
11 use WPStaging\Framework\Exceptions\WPStagingException;
12 use WPStaging\Framework\Filesystem\Filesystem;
13 use WPStaging\Framework\Filesystem\Filters\ExcludeFilter;
14 use WPStaging\Framework\Filesystem\PathChecker;
15 use WPStaging\Framework\Filesystem\PathIdentifier;
16 use WPStaging\Framework\SiteInfo;
17 use WPStaging\Framework\TemplateEngine\TemplateEngine;
18 use WPStaging\Framework\Utils\Strings;
19 use WPStaging\Core\WPStaging;
20 use WPStaging\Staging\Dto\DirectoryNodeDto;
21 use WPStaging\Staging\Sites;
22
23
24
25
26 class DirectoryScanner
27 {
28
29
30
31
32
33
34 const WP_CORE_DIR = "wpstg-wp-core-dir";
35
36
37
38
39
40
41
42 const WP_NON_CORE_DIR = "wpstg-wp-non-core-dir";
43
44
45
46
47 protected $templateEngine;
48
49
50
51
52 protected $directory;
53
54
55
56
57 protected $strUtils;
58
59
60
61
62 protected $pathChecker;
63
64
65
66
67 protected $siteInfo;
68
69
70
71
72 protected $stagingSetup;
73
74
75
76
77
78
79 private $stagingSiteDirectories = null;
80
81
82
83
84 protected $loaderIcon = '';
85
86
87
88
89 protected $infoIcon = '';
90
91
92
93
94 protected $isAllowVfsPath = false;
95
96
97
98
99 protected $scanSubWpContentByDefault = false;
100
101
102
103
104 protected $excludedDirectories = [];
105
106
107
108
109 protected $extraDirectories = [];
110
111
112 protected $absPath = ABSPATH;
113
114
115 protected $wpContentPath = WP_CONTENT_DIR;
116
117
118 protected $filesystem;
119
120
121 protected $useDefaultSelection = false;
122
123
124
125
126 protected $showFileDestination = true;
127
128 public function __construct(TemplateEngine $templateEngine, Assets $assets, Directory $directory, Strings $strUtils, PathChecker $pathChecker, SiteInfo $siteInfo, Filesystem $filesystem)
129 {
130 $this->templateEngine = $templateEngine;
131 $this->directory = $directory;
132 $this->strUtils = $strUtils;
133 $this->pathChecker = $pathChecker;
134 $this->siteInfo = $siteInfo;
135 $this->loaderIcon = $assets->getAssetsUrl('img/spinner.gif');
136 $this->filesystem = $filesystem;
137
138
139 $this->absPath = $this->filesystem->normalizePath($this->absPath, true);
140 $this->wpContentPath = $this->filesystem->normalizePath($this->wpContentPath);
141 }
142
143
144
145
146
147 public function setIsAllowVfsPath(bool $isAllowVfsPath)
148 {
149 $this->isAllowVfsPath = $isAllowVfsPath;
150 }
151
152
153
154
155 public function setStagingSetup(AbstractStagingSetup $stagingSetup)
156 {
157 $this->stagingSetup = $stagingSetup;
158 $this->excludedDirectories = $stagingSetup->getStagingSiteDto()->getExcludedDirectories();
159 $this->extraDirectories = $stagingSetup->getStagingSiteDto()->getExtraDirectories();
160 }
161
162
163
164
165
166 public function setShowFileDestination(bool $showFileDestination)
167 {
168 $this->showFileDestination = $showFileDestination;
169 }
170
171 public function isUpdateOrResetJob(): bool
172 {
173 return $this->stagingSetup->isUpdateOrResetJob();
174 }
175
176 public function renderFilesSelection()
177 {
178 $directories = $this->scanDirectory($this->absPath, $this->absPath, PathIdentifier::IDENTIFIER_ABSPATH);
179
180
181 if ($this->isWpContentOutsideAbspath()) {
182 $wpContentDirectories = $this->scanDirectory(dirname($this->wpContentPath), $this->wpContentPath, PathIdentifier::IDENTIFIER_WP_CONTENT);
183 $directories = array_merge($directories, $wpContentDirectories);
184 }
185
186
187 $this->useDefaultSelection = true;
188
189 $result = $this->templateEngine->render('staging/_partials/files-selection.php', [
190 'scanner' => $this,
191 'stagingSetup' => $this->stagingSetup,
192 'stagingSiteDto' => $this->stagingSetup->getStagingSiteDto(),
193 'directories' => $directories,
194 'excludeFilters' => new ExcludeFilter(),
195 'showFileDestination' => $this->showFileDestination,
196 ]);
197
198 echo $result; // phpcs:ignore
199 }
200
201
202
203
204
205
206
207 public function scanDirectory(string $dirToScan, string $basePath, string $identifier): array
208 {
209 if (!is_dir($dirToScan)) {
210 throw new WPStagingException("The directory at path '{$dirToScan}' does not exist.");
211 }
212
213 try {
214 $iterator = new DirectoryIterator($dirToScan);
215 } catch (Throwable $ex) {
216 $errorMessage = $ex->getMessage();
217 if ($ex->getCode() === 5) {
218 $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');
219 }
220
221 throw new WPStagingException($errorMessage);
222 }
223
224 $directories = [];
225 foreach ($iterator as $directory) {
226 try {
227 if ($directory->isDot() || $directory->isFile()) {
228 continue;
229 }
230 } catch (RuntimeException $openBaseDirException) {
231 continue;
232 }
233
234 $directoryPath = $directory->getPathname();
235 try {
236 $path = $this->getPath($directory, $basePath, $identifier);
237 } catch (UnexpectedValueException $e) {
238 continue;
239 }
240
241 $directoryNode = new DirectoryNodeDto();
242 $directoryNode->setName($directory->getFilename());
243
244 if (strpos($directoryPath, 'wp-content') !== false && is_link($directoryPath)) {
245 $directoryNode->setPath(realpath($directory->getPathname()));
246 } elseif (is_link($directoryPath)) {
247
248
249 $directoryNode->setPath(wp_normalize_path($directoryPath));
250 } else {
251 $directoryNode->setPath(trailingslashit($basePath) . ltrim($path, '/'));
252 }
253
254 $directoryNode->setIdentifier($identifier);
255 $directoryNode->setBasePath($basePath);
256
257 $directories[$directory->getFilename()] = $directoryNode;
258 }
259
260 return $directories;
261 }
262
263
264
265
266
267
268
269 public function directoryListing(array $directories, bool $parentChecked = true, bool $preserveSelection = false): string
270 {
271 uksort($directories, 'strcasecmp');
272
273 $output = '';
274 foreach ($directories as $dirName => $directory) {
275
276 if (basename($dirName) === "\\") {
277 continue;
278 }
279
280 $output .= $this->renderDirectoryNode($directory, $parentChecked, $preserveSelection);
281 }
282
283 return $output;
284 }
285
286
287
288
289
290
291
292 protected function getPath(DirectoryIterator $directory, string $basePath, string $identifier): string
293 {
294 try {
295 $realPath = $this->isAllowVfsPath && strpos($directory->getPathname(), 'vfs://') === 0 ? $directory->getPathname() : $directory->getRealPath();
296 } catch (RuntimeException $openBaseDirException) {
297 throw new UnexpectedValueException($openBaseDirException->getMessage());
298 }
299
300 if ($realPath === false) {
301 throw new UnexpectedValueException("The path '{$directory->getPathname()}' could not be resolved, likely a dangling symlink.");
302 }
303
304 $realPath = $this->filesystem->normalizePath($realPath);
305
306
307
308
309
310
311 $path = $this->stripBasePath($realPath, $basePath);
312
313 try {
314 $isDir = $directory->isDir();
315 } catch (RuntimeException $openBaseDirException) {
316 throw new UnexpectedValueException($openBaseDirException->getMessage());
317 }
318
319
320 if (!$isDir || (strlen($path) < 1 && $identifier !== PathIdentifier::IDENTIFIER_WP_CONTENT)) {
321 throw new UnexpectedValueException("The path '{$path}' is not a valid directory.");
322 }
323
324 return $path;
325 }
326
327
328
329
330
331
332
333
334
335 protected function stripBasePath(string $realPath, string $basePath): string
336 {
337 $isUncBasePath = strpos($basePath, '//') === 0;
338 $startsWithBasePath = $isUncBasePath ? stripos($realPath, $basePath) === 0 : strpos($realPath, $basePath) === 0;
339 if (!$startsWithBasePath) {
340 throw new UnexpectedValueException("The directory at path '{$realPath}' is not within the base path '{$basePath}'.");
341 }
342
343 return substr($realPath, strlen($basePath));
344 }
345
346
347
348
349
350
351
352 protected function renderDirectoryNode(DirectoryNodeDto $directory, bool $parentChecked = true, bool $preserveSelection = false): string
353 {
354 $path = wp_normalize_path($directory->getPath());
355 $relPath = str_replace($directory->getBasePath(), '', $path);
356 $relPath = ltrim($relPath, '/');
357
358
359 $isNotWPCoreDir = $this->isNonWpCoreDirectory($directory->getName(), $path);
360
361 $class = $isNotWPCoreDir ? self::WP_NON_CORE_DIR : self::WP_CORE_DIR;
362 $dirType = $this->getDirectoryType($path);
363 $isScanned = 'false';
364 $normalizedPath = trailingslashit($path);
365 if (
366 $normalizedPath === $this->directory->getWpContentDirectory()
367 || $normalizedPath === $this->directory->getPluginsDirectory()
368 || $normalizedPath === $this->directory->getActiveThemeParentDirectory()
369 ) {
370 $isScanned = 'true';
371 }
372
373 $showChildByDefault = false;
374 if ($this->scanSubWpContentByDefault && ($normalizedPath === $this->wpContentPath . 'plugins/' || $normalizedPath === $this->wpContentPath . 'themes/' || $normalizedPath === $this->wpContentPath . 'uploads/')) {
375 $isScanned = 'true';
376 $showChildByDefault = true;
377 }
378
379
380
381
382
383 $normalizedBasePath = untrailingslashit($directory->getBasePath());
384 $isNavigatable = 'true';
385 if ($this->strUtils->startsWith($path, $normalizedBasePath . "/wp-admin") !== false || $this->strUtils->startsWith($path, $normalizedBasePath . "/wp-includes") !== false) {
386 $isNavigatable = 'false';
387 }
388
389
390 $shouldBeChecked = $this->useDefaultSelection ? !$isNotWPCoreDir : $parentChecked;
391 if (!$preserveSelection && $this->isUpdateOrResetJob() && (!$this->isPathInDirectories($path, $this->excludedDirectories, $directory->getBasePath()))) {
392 $shouldBeChecked = true;
393 } elseif (!$preserveSelection && $this->isUpdateOrResetJob()) {
394 $shouldBeChecked = false;
395 }
396
397 if (!$preserveSelection && $this->isUpdateOrResetJob() && $class === self::WP_NON_CORE_DIR && !$this->isPathInDirectories($path, $this->extraDirectories)) {
398 $shouldBeChecked = false;
399 }
400
401 $shouldBeChecked = $this->getShouldBeChecked($shouldBeChecked, $directory);
402 $isDisabledDir = $directory->getName() === 'wp-admin' || $directory->getName() === 'wp-includes';
403
404 $isDisabled = false;
405 if (strpos($directory->getPath(), 'wp-content/' . Directory::STAGING_SITE_DIRECTORY) !== false) {
406 $isDisabled = true;
407 $shouldBeChecked = false;
408 }
409
410 $isLink = false;
411 if (strpos(trailingslashit($directory->getBasePath()) . $directory->getName(), 'wp-content') !== false && is_link(trailingslashit($directory->getBasePath()) . $directory->getName())) {
412 $isDisabled = true;
413 $isNavigatable = 'false';
414 $shouldBeChecked = true;
415 $isLink = true;
416 $relPath = 'wp-content';
417 }
418
419 if ($this->isMandatoryExcludedDirectory($path)) {
420 $isDisabled = true;
421 $shouldBeChecked = false;
422 }
423
424
425
426
427 $isLeaf = false;
428 if ($isNavigatable === 'true' && !$this->directoryHasSubdirectories($path)) {
429 $isLeaf = true;
430 $isNavigatable = 'false';
431 }
432
433
434
435 $isStagingSite = in_array(rtrim($path, '/\\'), $this->getStagingSiteDirectories(), true);
436
437 return $this->templateEngine->render('staging/_partials/directory-navigation.php', [
438 'scanner' => $this,
439 'prefix' => $directory->getIdentifier(),
440 'relPath' => $relPath,
441 'class' => $class,
442 'dirType' => $dirType,
443 'isScanned' => $isScanned,
444 'isNavigatable' => $isNavigatable,
445 'isLeaf' => $isLeaf,
446 'isStagingSite' => $isStagingSite,
447 'shouldBeChecked' => $shouldBeChecked,
448 'parentChecked' => $parentChecked,
449 'directoryDisabled' => $isNotWPCoreDir || $isDisabledDir,
450 'isDisabled' => $isDisabled,
451 'dirName' => $directory->getName(),
452 'gifLoaderPath' => $this->loaderIcon,
453 'infoIconPath' => $this->infoIcon,
454 'isDebugMode' => false,
455 'dataPath' => $directory->getPath(),
456 'basePath' => $directory->getBasePath(),
457 'forceDefault' => $preserveSelection,
458 'dirPath' => $path,
459 'isLink' => $isLink,
460 'showChild' => $showChildByDefault,
461 ]);
462 }
463
464
465
466
467
468
469
470
471 private function directoryHasSubdirectories(string $path): bool
472 {
473 if (!is_dir($path)) {
474 return false;
475 }
476
477 try {
478 $iterator = new DirectoryIterator($path);
479 foreach ($iterator as $item) {
480 if ($item->isDot()) {
481 continue;
482 }
483
484 if ($item->isDir()) {
485 return true;
486 }
487 }
488 } catch (\Exception $e) {
489
490
491 return true;
492 }
493
494 return false;
495 }
496
497
498
499
500
501
502
503 private function getStagingSiteDirectories(): array
504 {
505 if ($this->stagingSiteDirectories !== null) {
506 return $this->stagingSiteDirectories;
507 }
508
509 $this->stagingSiteDirectories = [];
510 try {
511 foreach (WPStaging::make(Sites::class)->getStagingDirectories() as $directory) {
512 $this->stagingSiteDirectories[] = wp_normalize_path(rtrim((string)$directory, '/\\'));
513 }
514 } catch (\Throwable $e) {
515 $this->stagingSiteDirectories = [];
516 }
517
518 return $this->stagingSiteDirectories;
519 }
520
521
522
523
524
525
526
527
528 protected function isNonWpCoreDirectory(string $dirname, string $path): bool
529 {
530 $coreDirectories = [
531 'wp-admin',
532 'wp-content',
533 'wp-includes',
534 ];
535
536 if (in_array($dirname, $coreDirectories)) {
537 return false;
538 }
539
540 $wpDirectories = [
541 $this->directory->getWpContentDirectory(),
542 $this->directory->getPluginsDirectory(),
543 $this->directory->getActiveThemeParentDirectory(),
544 $this->directory->getUploadsDirectory(),
545 $this->directory->getMuPluginsDirectory(),
546 ];
547
548 foreach ($wpDirectories as $wpDirectory) {
549 if (strpos(trailingslashit($path), $wpDirectory) !== false) {
550 return false;
551 }
552 }
553
554 return true;
555 }
556
557
558
559
560
561
562
563
564
565 protected function isPathInDirectories(string $path, array $directories, $basePath = null): bool
566 {
567 return $this->pathChecker->isPathInPathsList($path, $directories, true, $basePath);
568 }
569
570
571
572
573
574 protected function isMandatoryExcludedDirectory(string $path): bool
575 {
576 $path = untrailingslashit(wp_normalize_path($path));
577 foreach ($this->getMandatoryExcludedDirectories() as $excludedDirectory) {
578 if ($path === untrailingslashit(wp_normalize_path($excludedDirectory))) {
579 return true;
580 }
581 }
582
583 return false;
584 }
585
586
587
588
589 protected function getMandatoryExcludedDirectories(): array
590 {
591 return $this->directory->getWpStagingDataDirectories();
592 }
593
594 protected function isWpContentOutsideAbspath(): bool
595 {
596 return $this->siteInfo->isWpContentOutsideAbspath();
597 }
598
599 protected function isCheckDirectorySize(): bool
600 {
601 return false;
602 }
603
604
605
606
607 protected function getShouldBeChecked(bool $shouldBeChecked, DirectoryNodeDto $directory): bool
608 {
609 return $shouldBeChecked;
610 }
611
612
613
614
615 protected function getDirectoryType(string $path): string
616 {
617 $dirType = 'other';
618 if ($this->strUtils->startsWith($path, $this->directory->getPluginsDirectory()) !== false) {
619 $pluginPath = $this->strUtils->strReplaceFirst($this->directory->getPluginsDirectory(), '', $path);
620 $dirType = strpos($pluginPath, '/') === false ? 'plugin' : 'other';
621 } elseif ($this->strUtils->startsWith($path, $this->directory->getActiveThemeParentDirectory()) !== false) {
622 $themePath = $this->strUtils->strReplaceFirst($this->directory->getActiveThemeParentDirectory(), '', $path);
623 $dirType = strpos($themePath, '/') === false ? 'theme' : 'other';
624 }
625
626 return $dirType;
627 }
628 }
629