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