PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.3.0
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.3.0
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 / Framework / Filesystem / FilesystemScanner.php
wp-staging / Framework / Filesystem Last commit date
Filters 1 year ago Scanning 5 years ago AbstractFileObject.php 1 year ago AbstractFilesystemScanner.php 1 year ago DebugLogReader.php 2 years ago DirectoryListing.php 2 years ago DiskWriteCheck.php 1 year ago FileObject.php 1 year ago Filesystem.php 1 year ago FilesystemExceptions.php 5 years ago FilesystemScanner.php 10 months ago FilesystemScannerDto.php 1 year ago FilterableDirectoryIterator.php 1 year ago LogCleanup.php 2 years ago LogFiles.php 1 year ago MissingFileException.php 3 years ago OPcache.php 2 years ago PartIdentifier.php 11 months ago PathChecker.php 2 years ago PathIdentifier.php 1 year ago Permissions.php 1 year ago WpUploadsFolderSymlinker.php 4 years ago
FilesystemScanner.php
420 lines
1 <?php
2
3 namespace WPStaging\Framework\Filesystem;
4
5 use OutOfBoundsException;
6 use RuntimeException;
7 use SplFileInfo;
8 use Throwable;
9 use WPStaging\Core\WPStaging;
10 use WPStaging\Framework\Adapter\Directory;
11 use WPStaging\Framework\Job\Exception\DiskNotWritableException;
12 use WPStaging\Framework\Queue\FinishedQueueException;
13 use WPStaging\Framework\Queue\SeekableQueueInterface;
14 use WPStaging\Framework\SiteInfo;
15 use WPStaging\Framework\Utils\PluginInfo;
16 use WPStaging\Vendor\Psr\Log\LoggerInterface;
17
18 class FilesystemScanner extends AbstractFilesystemScanner
19 {
20 /** @var SeekableQueueInterface */
21 protected $filesystemQueue;
22
23 /** @var SeekableQueueInterface */
24 protected $taskQueue;
25
26 /** @var LoggerInterface */
27 protected $logger;
28
29 /** @var FilesystemScannerDto */
30 protected $scannerDto;
31
32 /** @var string */
33 protected $logTitle = '';
34
35 /** @var string */
36 protected $queueCacheName = '';
37
38 /** @var int */
39 protected $ignoreFileBiggerThan = 0;
40
41 /** @var array */
42 protected $ignoreFileExtensions = [];
43
44 /** @var array */
45 protected $ignoreFileExtensionFilesBiggerThan = [];
46
47 /** @var bool */
48 protected $isSiteHostedOnWordPressCom = false;
49
50 /**
51 * @param Directory $directory
52 * @param PathIdentifier $pathIdentifier
53 * @param Filesystem $filesystem
54 * @param PluginInfo $pluginInfo
55 * @param SiteInfo $siteInfo
56 * @param SeekableQueueInterface $filesystemQueue
57 */
58 public function __construct(
59 Directory $directory,
60 PathIdentifier $pathIdentifier,
61 Filesystem $filesystem,
62 PluginInfo $pluginInfo,
63 SiteInfo $siteInfo,
64 SeekableQueueInterface $filesystemQueue
65 ) {
66 parent::__construct($directory, $pathIdentifier, $filesystem, $pluginInfo);
67 $this->isSiteHostedOnWordPressCom = $siteInfo->isHostedOnWordPressCom();
68 $this->filesystemQueue = $filesystemQueue;
69 }
70
71 /**
72 * @param int $ignoreFileBiggerThan
73 * @param array $ignoreFileExtensions
74 * @param array $ignoreFileExtensionFilesBiggerThan
75 * @return void
76 */
77 public function setFilters(int $ignoreFileBiggerThan, array $ignoreFileExtensions, array $ignoreFileExtensionFilesBiggerThan)
78 {
79 $this->ignoreFileBiggerThan = $ignoreFileBiggerThan;
80 $this->ignoreFileExtensions = $ignoreFileExtensions;
81 $this->ignoreFileExtensionFilesBiggerThan = $ignoreFileExtensionFilesBiggerThan;
82 }
83
84 /**
85 * @return void
86 */
87 public function setupFilesystemQueue()
88 {
89 $fileBackupQueueCacheName = $this->queueCacheName . '_' . $this->currentPathScanning;
90 $this->filesystemQueue->setup($fileBackupQueueCacheName, SeekableQueueInterface::MODE_WRITE);
91 }
92
93 /**
94 * @param string $logTitle
95 * @return void
96 */
97 public function setLogTitle(string $logTitle)
98 {
99 $this->logTitle = $logTitle;
100 }
101
102 /**
103 * @param string $queueCacheName
104 * @return void
105 */
106 public function setQueueCacheName(string $queueCacheName)
107 {
108 $this->queueCacheName = $queueCacheName;
109 }
110
111 /**
112 * @param LoggerInterface $logger
113 * @param SeekableQueueInterface $taskQueue
114 * @param FilesystemScannerDto $scannerDto
115 * @return void
116 */
117 public function inject(LoggerInterface $logger, SeekableQueueInterface $taskQueue, FilesystemScannerDto $scannerDto)
118 {
119 $this->logger = $logger;
120 $this->taskQueue = $taskQueue;
121 $this->scannerDto = $scannerDto;
122 }
123
124 public function getFilesystemScannerDto(): FilesystemScannerDto
125 {
126 return $this->scannerDto;
127 }
128
129 /**
130 * @return void
131 */
132 public function unlockQueue()
133 {
134 $this->filesystemQueue->shutdown();
135 }
136
137 /**
138 * @return void
139 * @throws FinishedQueueException
140 * @throws DiskNotWritableException
141 */
142 public function processQueue()
143 {
144 try {
145 $path = $this->taskQueue->dequeue();
146 if ($path === null) {
147 throw new FinishedQueueException('Directory Scanner Queue is Finished');
148 }
149
150 $this->processPath($path);
151 } catch (FinishedQueueException $ex) {
152 try {
153 WPStaging::make(DiskWriteCheck::class)->checkPathCanStoreEnoughBytes($this->directory->getPluginUploadsDirectory(), $this->scannerDto->getFilesystemSize());
154 } catch (DiskNotWritableException $e) {
155 throw $e;
156 } catch (RuntimeException $e) {
157 // soft error, no action needed, but log
158 $this->logger->debug($e->getMessage());
159 }
160
161 throw $ex;
162 } catch (OutOfBoundsException $e) {
163 $this->logger->debug($e->getMessage());
164 } catch (Throwable $e) {
165 $this->logger->warning($e->getMessage());
166 }
167 }
168
169 /**
170 * @return void
171 */
172 protected function preRecursivePathScanningStep()
173 {
174 $this->setupFilesystemQueue();
175 }
176
177 /**
178 * @param SplFileInfo $file
179 * @param string $linkPath
180 * @return void
181 * @throws FinishedQueueException
182 */
183 protected function processFile(SplFileInfo $file, string $linkPath = '')
184 {
185 $normalizedPath = $this->filesystem->normalizePath($file->getPathname(), !$file->isFile());
186 $fileSize = $file->getSize();
187
188 $fileExtension = $file->getExtension();
189
190 // Lazy-built relative path
191 $relativePath = str_replace($this->filesystem->normalizePath(ABSPATH, true), '', $normalizedPath);
192
193 if ($this->canExcludeLogFile($fileExtension) || $this->canExcludeCacheFile($fileExtension) || isset($this->ignoreFileExtensions[$fileExtension])) {
194 // Early bail: File has an ignored extension
195 $this->logger->notice(sprintf(
196 '%s: Skipped file: "%s". Extension: "%s" is excluded by rule.',
197 esc_html($this->logTitle),
198 esc_html($relativePath),
199 esc_html($fileExtension)
200 ));
201
202 return;
203 }
204
205 if (isset($this->ignoreFileExtensionFilesBiggerThan[$fileExtension])) {
206 if ($fileSize > $this->ignoreFileExtensionFilesBiggerThan[$fileExtension]) {
207 // Early bail: File bigger than expected for given extension
208 $this->logger->notice(sprintf(
209 '%s: Skipped file "%s" (%s). It exceeds the maximum allowed file size for files with the extension "%s" (%s).',
210 esc_html($this->logTitle),
211 esc_html($relativePath),
212 size_format($fileSize),
213 esc_html($fileExtension),
214 size_format($this->ignoreFileExtensionFilesBiggerThan[$fileExtension])
215 ));
216
217 return;
218 }
219 } elseif ($fileSize > $this->ignoreFileBiggerThan) {
220 // Early bail: File is larger than max allowed size.
221 $this->logger->notice(sprintf(
222 '%s: Skipped file "%s" (%s). It exceeds the maximum file size (%s).',
223 esc_html($this->logTitle),
224 esc_html($relativePath),
225 size_format($fileSize),
226 size_format($this->ignoreFileBiggerThan)
227 ));
228
229 return;
230 }
231
232 $this->scannerDto->incrementDiscoveredFiles();
233 $this->scannerDto->incrementDiscoveredFilesByCategory($this->currentPathScanning);
234 $this->scannerDto->addFilesystemSize($fileSize);
235
236 if (!empty($linkPath)) {
237 $linkPath = $this->filesystem->normalizePath($linkPath, true);
238 $relativePath = $this->replaceEOLsWithPlaceholders($relativePath);
239 $path = rtrim($relativePath, '/') . self::PATH_SEPARATOR . rtrim($linkPath, '/');
240 $this->filesystemQueue->enqueue($path);
241 return;
242 }
243
244 $relativePath = $this->replaceEOLsWithPlaceholders($relativePath);
245 $this->filesystemQueue->enqueue(rtrim($relativePath, '/'));
246 }
247
248 /**
249 * @param SplFileInfo $dir
250 * @param SplFileInfo|null $link
251 * @return void
252 */
253 protected function processDirectory(SplFileInfo $dir, $link = null)
254 {
255 if ($this->isUploadsYearMonthDirectory($dir)) {
256 $this->preScanPath($dir->getPathname());
257 return;
258 }
259
260 $normalizedPath = $this->filesystem->normalizePath($dir->getPathname(), true);
261
262 if ($this->isExcludedDirectory($dir->getPathname()) || $this->canExcludeCacheDir($dir)) {
263 return;
264 }
265
266 if ($link !== null && $this->isExcludedDirectory($link->getPathname())) {
267 return;
268 }
269
270 if ($link !== null) {
271 $linkPath = $this->filesystem->normalizePath($link->getPathname(), true);
272 $this->taskQueue->enqueue($this->currentPathScanning . self::PATH_SEPARATOR . $normalizedPath . self::PATH_SEPARATOR . $linkPath);
273 return;
274 }
275
276 // we need to know
277 $this->taskQueue->enqueue($this->currentPathScanning . self::PATH_SEPARATOR . $normalizedPath);
278 }
279
280 /**
281 * @param string $path
282 * @return bool
283 */
284 protected function isExcludedDirectory(string $path): bool
285 {
286 $normalizedPath = $this->filesystem->normalizePath($path, true);
287
288 if (in_array($normalizedPath, $this->scannerDto->getExcludedDirectories())) {
289 $relativePathForLogging = str_replace($this->filesystem->normalizePath(WP_CONTENT_DIR, true), '', $normalizedPath);
290
291 $this->logger->notice(sprintf(
292 '%s: Skipped directory "%s". Excluded by rule',
293 esc_html($this->logTitle),
294 esc_html($relativePathForLogging)
295 ));
296
297 return true;
298 }
299
300 return false;
301 }
302
303 /**
304 * RecursivePathScanning method extended to include exclude filter and directory increment
305 * @inheritdoc
306 */
307 protected function recursivePathScanning(string $path, string $link = '')
308 {
309 if ($this->isExcludedDirectory($path)) {
310 return;
311 }
312
313 $this->scannerDto->incrementTotalDirectories();
314
315 parent::recursivePathScanning($path, $link);
316 }
317
318 /**
319 * @param \SplFileInfo $dir
320 * @return bool
321 */
322 protected function isUploadsYearMonthDirectory(SplFileInfo $dir): bool
323 {
324 if ($this->currentPathScanning !== PartIdentifier::UPLOAD_PART_IDENTIFIER) {
325 return false;
326 }
327
328 $parentDir = $dir->getPathInfo();
329 if ($parentDir === false) {
330 return false;
331 }
332
333 if ($this->filesystem->normalizePath($parentDir->getPathname(), true) !== $this->directory->getUploadsDirectory()) {
334 return false;
335 }
336
337 /**
338 * This is a default WordPress year-month uploads folder.
339 *
340 * Here we break down the uploads folder by months, considering it's often the largest folder in a website,
341 * and we need to be able to scan each folder in one request.
342 */
343 return is_numeric($dir->getBasename()) && $dir->getBasename() > 1970 && $dir->getBasename() < 2100;
344 }
345
346 /**
347 * @param string $fileExtension
348 * @return bool
349 */
350 private function canExcludeLogFile(string $fileExtension): bool
351 {
352 if ($fileExtension !== 'log') {
353 return false;
354 }
355
356 if (!$this->scannerDto->getIsExcludingLogs()) {
357 return false;
358 }
359
360 return true;
361 }
362
363 /**
364 * @param string $fileExtension
365 * @return bool
366 */
367 private function canExcludeCacheFile(string $fileExtension): bool
368 {
369 if ($fileExtension !== 'cache') {
370 return false;
371 }
372
373 if (!$this->scannerDto->getIsExcludingCaches()) {
374 return false;
375 }
376
377 return true;
378 }
379
380 /**
381 * @param SplFileInfo $dir
382 * @return bool
383 */
384 private function canExcludeCacheDir(SplFileInfo $dir): bool
385 {
386 if (!$dir->isDir()) {
387 return false;
388 }
389
390 if (!$this->scannerDto->getIsExcludingCaches()) {
391 return false;
392 }
393
394 if (!$this->isPathContainsCache($dir->getRealPath())) {
395 return false;
396 }
397
398 $this->logger->notice(sprintf(
399 '%s: Skipped directory "%s". Excluded by smart exclusion rule: Excluding cache folder.',
400 esc_html($this->logTitle),
401 esc_html($dir->getRealPath())
402 ));
403
404 return true;
405 }
406
407 /**
408 * Check if "cache" is one of the directory names.
409 *
410 * @param string $path
411 * @return bool
412 */
413 private function isPathContainsCache(string $path): bool
414 {
415 $pathParts = explode(DIRECTORY_SEPARATOR, $path);
416
417 return in_array('cache', $pathParts);
418 }
419 }
420