Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.35% covered (success)
95.35%
41 / 43
88.89% covered (success)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Filesystem
95.35% covered (success)
95.35%
41 / 43
88.89% covered (success)
88.89%
8 / 9
27
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 getRootPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 recursiveCopy
88.24% covered (success)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
6.06
 createDirectory
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 moveDirectory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deleteDirectory
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
8
 copy
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3/**
4 * Class for filesystem operations.
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 2012-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     2012-04-02
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Filesystem;
21
22use phpMyFAQ\Core\Exception;
23
24class Filesystem
25{
26    private readonly string $rootPath;
27
28    private string $path;
29
30    /**
31     * Constructor sets the root path of the primary phpMyFAQ installation.
32     */
33    public function __construct(string $rootPath = '')
34    {
35        $this->rootPath = $rootPath === '' || $rootPath === '0' ? dirname(__DIR__, levels: 2) : $rootPath;
36    }
37
38    public function getRootPath(): string
39    {
40        return $this->rootPath;
41    }
42
43    public function getPath(): string
44    {
45        return $this->path;
46    }
47
48    public function setPath(string $path): void
49    {
50        $this->path = $path;
51    }
52
53    /**
54     * Recursively copies the source to the destination.
55     * @throws Exception
56     */
57    public function recursiveCopy(string $source, string $dest): bool
58    {
59        if (!is_dir($source)) {
60            return false;
61        }
62
63        $directoryName = substr($source, (int) strrpos($source, needle: '/') + 1);
64        $this->createDirectory($dest . DIRECTORY_SEPARATOR . $directoryName, mode: 0o750, recursive: true);
65
66        $entries = scandir($source);
67        if ($entries === false) {
68            return false;
69        }
70
71        foreach (array_diff($entries, ['.', '..']) as $file) {
72            $sourcePath = $source . DIRECTORY_SEPARATOR . $file;
73            $targetPath = $dest . DIRECTORY_SEPARATOR . $directoryName . DIRECTORY_SEPARATOR . $file;
74
75            if (is_link($sourcePath)) {
76                continue;
77            }
78
79            if (is_dir($sourcePath)) {
80                $this->recursiveCopy($sourcePath, $dest . DIRECTORY_SEPARATOR . $directoryName);
81                continue;
82            }
83
84            // Default: try to copy, copy() validates readability and destination
85            $this->copy($sourcePath, $targetPath);
86        }
87
88        return true;
89    }
90
91    /**
92     * Creates directory.
93     */
94    public function createDirectory(string $pathname, int $mode = 0o777, bool $recursive = false): bool
95    {
96        if (is_dir($pathname)) {
97            return true;
98        }
99
100        return mkdir($pathname, $mode, $recursive);
101    }
102
103    /**
104     * Moves a given directory.
105     */
106    public function moveDirectory(string $sourcePath, string $destinationPath): bool
107    {
108        return rename($sourcePath, $destinationPath);
109    }
110
111    /**
112     * Deletes the given directory.
113     */
114    public function deleteDirectory(string $pathname): bool
115    {
116        // Guard for empty or invalid paths
117        if ($pathname === '' || $pathname === '0' || !is_dir($pathname)) {
118            return false;
119        }
120
121        $entries = scandir($pathname);
122        if ($entries === false) {
123            return false;
124        }
125
126        foreach (array_diff($entries, ['.', '..']) as $file) {
127            $full = $pathname . DIRECTORY_SEPARATOR . $file;
128
129            if (is_dir($full) && !is_link($full)) {
130                $this->deleteDirectory($full);
131                continue;
132            }
133
134            unlink($full);
135        }
136
137        return rmdir($pathname);
138    }
139
140    /**
141     * Copies the source file to the destination file.
142     *
143     * @throws Exception
144     */
145    public function copy(string $sourceFileName, string $destinationFileName): bool
146    {
147        if (!is_readable($sourceFileName) || !is_writable(dirname($destinationFileName))) {
148            throw new Exception(message: 'Source not readable or destination directory not writeable.');
149        }
150
151        if (copy($sourceFileName, $destinationFileName) === false) {
152            $error = error_get_last();
153            throw new Exception($error['message'] ?? 'Copy operation failed.');
154        }
155
156        return true;
157    }
158}