Lines 90.47% 19 / 21
Functions and Methods 50.00% 1 / 2
Classes and Traits 0.00% 0 / 1
Covered by tests of size
Name Lines Functions and Methods CRAP Classes and Traits
WritablePathScanner 90.47% 19 / 21 50.00% 1 / 2 8.06 0.00% 0 / 1
 getNonWritablePaths 87.50% 14 / 16 0.00% 0 / 1 6.07
 formatPathList 100.00% 5 / 5 100.00% 1 / 1 2
1<?php
2
3/**
4 * Scans a directory tree for paths the current process cannot write to.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2026-08-07
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Setup;
21
22use FilesystemIterator;
23use RecursiveDirectoryIterator;
24use RecursiveIteratorIterator;
25use SplFileInfo;
26
27final class WritablePathScanner
28{
29    /**
30     * Returns all paths inside the given directory the current process cannot
31     * write to. Paths inside the excluded directory are skipped.
32     *
33     * @return string[]
34     */
35    public static function getNonWritablePaths(string $directory, string $excludedDirectory): array
36    {
37        $nonWritablePaths = [];
38        $realExcludedDirectory = realpath($excludedDirectory);
39
40        $items = new RecursiveIteratorIterator(
41            new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS),
42            RecursiveIteratorIterator::SELF_FIRST,
43            RecursiveIteratorIterator::CATCH_GET_CHILD,
44        );
45
46        foreach ($items as $item) {
47            if (!$item instanceof SplFileInfo) {
48                continue;
49            }
50
51            $path = $item->getPathname();
52
53            if ($realExcludedDirectory !== false && str_starts_with($path, $realExcludedDirectory)) {
54                continue;
55            }
56
57            if (!$item->isWritable()) {
58                $nonWritablePaths[] = $path;
59            }
60        }
61
62        return $nonWritablePaths;
63    }
64
65    /**
66     * Formats a list of paths for an error message, truncated to the first
67     * five entries.
68     *
69     * @param string[] $paths
70     */
71    public static function formatPathList(array $paths): string
72    {
73        $additionalPathCount = count($paths) - 5;
74
75        return (
76            implode(', ', array_slice($paths, offset: 0, length: 5))
77            . ($additionalPathCount > 0 ? sprintf(' and %d more', $additionalPathCount) : '')
78        );
79    }
80}