PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.8.1
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.8.1
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 2 months ago AbstractStagingSetup.php 4 months ago DirectoryScanner.php 4 months ago FileCopier.php 2 months ago StagingSetup.php 4 months ago TableScanner.php 8 months ago
DirectoryScanner.php
448 lines
1 <?php
2
3 namespace WPStaging\Staging\Service;
4
5 use DirectoryIterator;
6 use Throwable;
7 use UnexpectedValueException;
8 use WPStaging\Framework\Adapter\Directory;
9 use WPStaging\Framework\Assets\Assets;
10 use WPStaging\Framework\Exceptions\WPStagingException;
11 use WPStaging\Framework\Filesystem\Filters\ExcludeFilter;
12 use WPStaging\Framework\Filesystem\PathChecker;
13 use WPStaging\Framework\Filesystem\PathIdentifier;
14 use WPStaging\Framework\SiteInfo;
15 use WPStaging\Framework\TemplateEngine\TemplateEngine;
16 use WPStaging\Framework\Utils\Strings;
17 use WPStaging\Staging\Dto\DirectoryNodeDto;
18
19 /**
20 * Scans filesystem roots and prepares directory data for the staging selection UI.
21 */
22 class DirectoryScanner
23 {
24 /**
25 * CSS class name to use for WordPress core directories like wp-content, wp-admin, wp-includes
26 * This doesn't contain class selector prefix
27 *
28 * @var string
29 */
30 const WP_CORE_DIR = "wpstg-wp-core-dir";
31
32 /**
33 * CSS class name to use for WordPress non core directories
34 * This doesn't contain class selector prefix
35 *
36 * @var string
37 */
38 const WP_NON_CORE_DIR = "wpstg-wp-non-core-dir";
39
40 /**
41 * @var TemplateEngine
42 */
43 protected $templateEngine;
44
45 /**
46 * @var Directory
47 */
48 protected $directory;
49
50 /**
51 * @var Strings
52 */
53 protected $strUtils;
54
55 /**
56 * @var PathChecker
57 */
58 protected $pathChecker;
59
60 /**
61 * @var SiteInfo
62 */
63 protected $siteInfo;
64
65 /**
66 * @var AbstractStagingSetup
67 */
68 protected $stagingSetup;
69
70 /**
71 * @var string
72 */
73 protected $loaderIcon = '';
74
75 /**
76 * @var string
77 */
78 protected $infoIcon = '';
79
80 /**
81 * @var bool
82 */
83 protected $isAllowVfsPath = false;
84
85 /**
86 * @var bool
87 */
88 protected $scanSubWpContentByDefault = false;
89
90 /**
91 * @var array
92 */
93 protected $excludedDirectories = [];
94
95 /**
96 * @var array
97 */
98 protected $extraDirectories = [];
99
100 /** @var string */
101 protected $absPath = ABSPATH;
102
103 /** @var string */
104 protected $wpContentPath = WP_CONTENT_DIR;
105
106 /** @var bool */
107 protected $useDefaultSelection = false;
108
109 public function __construct(TemplateEngine $templateEngine, Assets $assets, Directory $directory, Strings $strUtils, PathChecker $pathChecker, SiteInfo $siteInfo)
110 {
111 $this->templateEngine = $templateEngine;
112 $this->directory = $directory;
113 $this->strUtils = $strUtils;
114 $this->pathChecker = $pathChecker;
115 $this->siteInfo = $siteInfo;
116 $this->loaderIcon = $assets->getAssetsUrl('img/spinner.gif');
117 }
118
119 /**
120 * @param bool $isAllowVfsPath
121 * @return void
122 */
123 public function setIsAllowVfsPath(bool $isAllowVfsPath)
124 {
125 $this->isAllowVfsPath = $isAllowVfsPath;
126 }
127
128 /**
129 * @return void
130 */
131 public function setStagingSetup(AbstractStagingSetup $stagingSetup)
132 {
133 $this->stagingSetup = $stagingSetup;
134 }
135
136 public function isUpdateOrResetJob(): bool
137 {
138 return $this->stagingSetup->isUpdateOrResetJob();
139 }
140
141 public function renderFilesSelection()
142 {
143 $directories = $this->scanDirectory($this->absPath, $this->absPath, PathIdentifier::IDENTIFIER_ABSPATH);
144
145 // If wp-content is outside ABSPATH, then scan it too
146 if ($this->isWpContentOutsideAbspath()) {
147 $wpContentDirectories = $this->scanDirectory(dirname($this->wpContentPath), $this->wpContentPath, PathIdentifier::IDENTIFIER_WP_CONTENT);
148 $directories = array_merge($directories, $wpContentDirectories);
149 }
150
151 /** Value of parent checked will be ignored instead the default selection will be used */
152 $this->useDefaultSelection = true;
153
154 $result = $this->templateEngine->render('staging/_partials/files-selection.php', [
155 'scanner' => $this,
156 'stagingSetup' => $this->stagingSetup,
157 'stagingSiteDto' => $this->stagingSetup->getStagingSiteDto(),
158 'directories' => $directories,
159 'excludeFilters' => new ExcludeFilter(),
160 ]);
161
162 echo $result; // phpcs:ignore
163 }
164
165 /**
166 * @param string $dirToScan
167 * @param string $basePath
168 * @param string $identifier
169 * @return DirectoryNodeDto[]
170 */
171 public function scanDirectory(string $dirToScan, string $basePath, string $identifier): array
172 {
173 if (!is_dir($dirToScan)) {
174 throw new WPStagingException("The directory at path '{$dirToScan}' does not exist.");
175 }
176
177 try {
178 $iterator = new DirectoryIterator($dirToScan);
179 } catch (Throwable $ex) {
180 $errorMessage = $ex->getMessage();
181 if ($ex->getCode() === 5) {
182 $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');
183 }
184
185 throw new WPStagingException($errorMessage);
186 }
187
188 $directories = [];
189 foreach ($iterator as $directory) {
190 if ($directory->isDot() || $directory->isFile()) {
191 continue;
192 }
193
194 // Not a valid directory
195 $path = $this->getPath($directory, $basePath, $identifier);
196 if (strpos($directory, 'wp-content') !== false && is_link($directory) && $path === false) {
197 continue;
198 }
199
200 $directoryNode = new DirectoryNodeDto();
201 $directoryNode->setName($directory->getFilename());
202
203 if (strpos($directory, 'wp-content') !== false && is_link($directory)) {
204 $directoryNode->setPath(realpath($directory->getPathname()));
205 } else {
206 $directoryNode->setPath(trailingslashit($basePath) . ltrim($path, '/'));
207 }
208
209 $directoryNode->setIdentifier($identifier);
210 $directoryNode->setBasePath($basePath);
211
212 $directories[$directory->getFilename()] = $directoryNode;
213 }
214
215 return $directories;
216 }
217
218 /**
219 * @param DirectoryNodeDto[] $directories
220 * @param bool $parentChecked
221 * @param bool $preserveSelection
222 * @return string
223 */
224 public function directoryListing(array $directories, bool $parentChecked = true, bool $preserveSelection = false): string
225 {
226 uksort($directories, 'strcasecmp');
227
228 $output = '';
229 foreach ($directories as $dirName => $directory) {
230 // Not a directory, possibly a symlink, therefore we will skip it
231 if (basename($dirName) === "\\") {
232 continue;
233 }
234
235 $output .= $this->renderDirectoryNode($directory, $parentChecked, $preserveSelection);
236 }
237
238 return $output;
239 }
240
241 /**
242 * @param DirectoryIterator $directory
243 * @param string $basePath
244 * @param string $identifier
245 * @return string
246 */
247 protected function getPath(DirectoryIterator $directory, string $basePath, string $identifier): string
248 {
249 $realPath = $this->isAllowVfsPath && strpos($directory->getPathname(), 'vfs://') === 0 ? $directory->getPathname() : $directory->getRealPath();
250 $realPath = wp_normalize_path($realPath);
251
252 /**
253 * Do not follow root path like src/web/..
254 * This must be done before \SplFileInfo->isDir() is used!
255 * Prevents open base dir restriction fatal errors
256 */
257 if (strpos($realPath, $basePath) !== 0) {
258 throw new UnexpectedValueException("The directory at path '{$realPath}' is not within the base path '{$basePath}'.");
259 }
260
261 $path = str_replace($basePath, '', $realPath);
262 // Using strpos() for symbolic links as they could create nasty stuff in nix stuff for directory structures
263 if (!$directory->isDir() || (strlen($path) < 1 && $identifier !== PathIdentifier::IDENTIFIER_WP_CONTENT)) {
264 throw new UnexpectedValueException("The path '{$path}' is not a valid directory.");
265 }
266
267 return $path;
268 }
269
270 /**
271 * @param DirectoryNodeDto $directory
272 * @param bool $parentChecked
273 * @param bool $preserveSelection
274 * @return string
275 */
276 protected function renderDirectoryNode(DirectoryNodeDto $directory, bool $parentChecked = true, bool $preserveSelection = false): string
277 {
278 $path = wp_normalize_path($directory->getPath());
279 $relPath = str_replace($directory->getBasePath(), '', $path);
280 $relPath = ltrim($relPath, '/');
281
282 // Check if directory name or directory path is not WP core folder
283 $isNotWPCoreDir = $this->isNonWpCoreDirectory($directory->getName(), $path);
284
285 $class = $isNotWPCoreDir ? self::WP_NON_CORE_DIR : self::WP_CORE_DIR;
286 $dirType = $this->getDirectoryType($path);
287 $isScanned = 'false';
288 $normalizedPath = trailingslashit($path);
289 if (
290 $normalizedPath === $this->directory->getWpContentDirectory()
291 || $normalizedPath === $this->directory->getPluginsDirectory()
292 || $normalizedPath === $this->directory->getActiveThemeParentDirectory()
293 ) {
294 $isScanned = 'true';
295 }
296
297 $showChildByDefault = false;
298 if ($this->scanSubWpContentByDefault && ($normalizedPath === $this->wpContentPath . 'plugins/' || $normalizedPath === $this->wpContentPath . 'themes/' || $normalizedPath === $this->wpContentPath . 'uploads/')) {
299 $isScanned = 'true';
300 $showChildByDefault = true;
301 }
302
303 // Make wp-includes and wp-admin directory items not expandable
304 $isNavigatable = 'true';
305 if ($this->strUtils->startsWith($path, $directory->getBasePath() . "/wp-admin") !== false || $this->strUtils->startsWith($path, $directory->getBasePath() . "/wp-includes") !== false) {
306 $isNavigatable = 'false';
307 }
308
309 // Decide if item checkbox is active or not
310 $shouldBeChecked = $this->useDefaultSelection ? !$isNotWPCoreDir : $parentChecked;
311 if (!$preserveSelection && $this->isUpdateOrResetJob() && (!$this->isPathInDirectories($path, $this->excludedDirectories, $directory->getBasePath()))) {
312 $shouldBeChecked = true;
313 } elseif (!$preserveSelection && $this->isUpdateOrResetJob()) {
314 $shouldBeChecked = false;
315 }
316
317 if (!$preserveSelection && $this->isUpdateOrResetJob() && $class === self::WP_NON_CORE_DIR && !$this->isPathInDirectories($path, $this->extraDirectories)) {
318 $shouldBeChecked = false;
319 }
320
321 $shouldBeChecked = $this->getShouldBeChecked($shouldBeChecked, $directory);
322 $isDisabledDir = $directory->getName() === 'wp-admin' || $directory->getName() === 'wp-includes';
323
324 $isDisabled = false;
325 if (strpos($directory->getPath(), 'wp-content/' . Directory::STAGING_SITE_DIRECTORY) !== false) {
326 $isDisabled = true;
327 $shouldBeChecked = false;
328 }
329
330 $isLink = false;
331 if (strpos(trailingslashit($directory->getBasePath()) . $directory->getName(), 'wp-content') !== false && is_link(trailingslashit($directory->getBasePath()) . $directory->getName())) {
332 $isDisabled = true;
333 $isNavigatable = 'false';
334 $shouldBeChecked = true;
335 $isLink = true;
336 $relPath = 'wp-content';
337 }
338
339 return $this->templateEngine->render('staging/_partials/directory-navigation.php', [
340 'scanner' => $this,
341 'prefix' => $directory->getIdentifier(),
342 'relPath' => $relPath,
343 'class' => $class,
344 'dirType' => $dirType,
345 'isScanned' => $isScanned,
346 'isNavigatable' => $isNavigatable,
347 'shouldBeChecked' => $shouldBeChecked,
348 'parentChecked' => $parentChecked,
349 'directoryDisabled' => $isNotWPCoreDir || $isDisabledDir,
350 'isDisabled' => $isDisabled,
351 'dirName' => $directory->getName(),
352 'gifLoaderPath' => $this->loaderIcon,
353 'infoIconPath' => $this->infoIcon,
354 'isDebugMode' => false,
355 'dataPath' => $directory->getPath(),
356 'basePath' => $directory->getBasePath(),
357 'forceDefault' => $preserveSelection,
358 'dirPath' => $path,
359 'isLink' => $isLink,
360 'showChild' => $showChildByDefault,
361 ]);
362 }
363
364 /**
365 * Check if directory name or directory path is not WP core folder
366 *
367 * @param string $dirname
368 * @param string $path
369 * @return bool
370 */
371 protected function isNonWpCoreDirectory(string $dirname, string $path): bool
372 {
373 $coreDirectories = [
374 'wp-admin',
375 'wp-content',
376 'wp-includes',
377 ];
378
379 if (in_array($dirname, $coreDirectories)) {
380 return false;
381 }
382
383 $wpDirectories = [
384 $this->directory->getWpContentDirectory(),
385 $this->directory->getPluginsDirectory(),
386 $this->directory->getActiveThemeParentDirectory(),
387 $this->directory->getUploadsDirectory(),
388 $this->directory->getMuPluginsDirectory(),
389 ];
390
391 foreach ($wpDirectories as $wpDirectory) {
392 if (strpos(trailingslashit($path), $wpDirectory) !== false) {
393 return false;
394 }
395 }
396
397 return true;
398 }
399
400 /**
401 * Is the path present is given list of directories
402 * @param string $path
403 * @param array $directories List of directories relative to ABSPATH with leading slash
404 * @param ?string $basePath
405 *
406 * @return bool
407 */
408 protected function isPathInDirectories(string $path, array $directories, $basePath = null): bool
409 {
410 return $this->pathChecker->isPathInPathsList($path, $directories, true, $basePath);
411 }
412
413 protected function isWpContentOutsideAbspath(): bool
414 {
415 return $this->siteInfo->isWpContentOutsideAbspath();
416 }
417
418 protected function isCheckDirectorySize(): bool
419 {
420 return false;
421 }
422
423 /**
424 * Used during push
425 */
426 protected function getShouldBeChecked(bool $shouldBeChecked, DirectoryNodeDto $directory): bool
427 {
428 return $shouldBeChecked;
429 }
430
431 /**
432 * Overriden during push
433 */
434 protected function getDirectoryType(string $path): string
435 {
436 $dirType = 'other';
437 if ($this->strUtils->startsWith($path, $this->directory->getPluginsDirectory()) !== false) {
438 $pluginPath = $this->strUtils->strReplaceFirst($this->directory->getPluginsDirectory(), '', $path);
439 $dirType = strpos($pluginPath, '/') === false ? 'plugin' : 'other';
440 } elseif ($this->strUtils->startsWith($path, $this->directory->getActiveThemeParentDirectory()) !== false) {
441 $themePath = $this->strUtils->strReplaceFirst($this->directory->getActiveThemeParentDirectory(), '', $path);
442 $dirType = strpos($themePath, '/') === false ? 'theme' : 'other';
443 }
444
445 return $dirType;
446 }
447 }
448