Passed
Push — master ( 93a3b2...44b2fb )
by
unknown
11:19 queued 50s
created

FileHelper::normalize()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 10
nc 4
nop 1
dl 0
loc 15
rs 9.6111
c 1
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\CoreBundle\Helpers;
8
9
use League\Flysystem\FilesystemException;
10
use League\Flysystem\FilesystemOperator;
11
12
/**
13
 * Centralized helper for file operations using Flysystem.
14
 * All paths are treated as relative to the injected FilesystemOperator root.
15
 */
16
final class FileHelper
17
{
18
    public function __construct(
19
        private readonly FilesystemOperator $resourceFilesystem
20
    ) {}
21
22
    /**
23
     * Normalize a relative path and prevent path traversal.
24
     * It removes backslashes, null bytes and resolves "." / ".." segments.
25
     */
26
    private function normalize(string $path): string
27
    {
28
        $path = ltrim(str_replace(["\\", "\0"], ['/', ''], $path), '/');
29
        $parts = [];
30
        foreach (explode('/', $path) as $seg) {
31
            if ($seg === '' || $seg === '.') {
32
                continue;
33
            }
34
            if ($seg === '..') {
35
                array_pop($parts);
36
                continue;
37
            }
38
            $parts[] = $seg;
39
        }
40
        return implode('/', $parts);
41
    }
42
43
    /**
44
     * Writes contents to a file (overwrites if exists).
45
     * Equivalent to file_put_contents().
46
     *
47
     * @throws FilesystemException
48
     */
49
    public function write(string $path, string $contents): void
50
    {
51
        $path = $this->normalize($path);
52
        $this->resourceFilesystem->write($path, $contents);
53
    }
54
55
    /**
56
     * Writes contents replacing existing file if present (delete + write).
57
     * Useful when an atomic-like replacement is desired.
58
     *
59
     * @throws FilesystemException
60
     */
61
    public function put(string $path, string $contents): void
62
    {
63
        $path = $this->normalize($path);
64
        if ($this->resourceFilesystem->fileExists($path)) {
65
            $this->resourceFilesystem->delete($path);
66
        }
67
        $this->resourceFilesystem->write($path, $contents);
68
    }
69
70
    /**
71
     * Reads the contents of a file as a string.
72
     * Equivalent to file_get_contents().
73
     *
74
     * @throws FilesystemException
75
     */
76
    public function read(string $path): string
77
    {
78
        $path = $this->normalize($path);
79
        return $this->resourceFilesystem->read($path);
80
    }
81
82
    /**
83
     * Deletes a file.
84
     * Equivalent to unlink().
85
     *
86
     * @throws FilesystemException
87
     */
88
    public function delete(string $path): void
89
    {
90
        $path = $this->normalize($path);
91
        $this->resourceFilesystem->delete($path);
92
    }
93
94
    /**
95
     * Checks if a file exists.
96
     * Equivalent to file_exists().
97
     *
98
     * @throws FilesystemException
99
     */
100
    public function exists(string $path): bool
101
    {
102
        $path = $this->normalize($path);
103
        return $this->resourceFilesystem->fileExists($path);
104
    }
105
106
    /**
107
     * Opens a file for reading as a stream resource.
108
     * Equivalent to fopen() in read mode.
109
     *
110
     * @return resource
111
     * @throws FilesystemException
112
     */
113
    public function readStream(string $path)
114
    {
115
        $path = $this->normalize($path);
116
        $stream = $this->resourceFilesystem->readStream($path);
117
        if (!\is_resource($stream)) {
118
            throw new \RuntimeException("Unable to open stream for: {$path}");
119
        }
120
        return $stream;
121
    }
122
123
    /**
124
     * Writes a stream resource to a file.
125
     * Equivalent to fwrite() when working with streams.
126
     *
127
     * @param resource $stream
128
     * @throws FilesystemException
129
     */
130
    public function writeStream(string $path, $stream): void
131
    {
132
        $path = $this->normalize($path);
133
        if (!\is_resource($stream)) {
134
            throw new \InvalidArgumentException('writeStream expects a valid stream resource.');
135
        }
136
        $this->resourceFilesystem->writeStream($path, $stream);
137
    }
138
139
    /**
140
     * Outputs the contents of a file directly to STDOUT.
141
     * Equivalent to readfile().
142
     *
143
     * @throws FilesystemException
144
     */
145
    public function streamToOutput(string $path): void
146
    {
147
        $path = $this->normalize($path);
148
        $stream = $this->readStream($path);
149
        try {
150
            fpassthru($stream);
151
        } finally {
152
            if (\is_resource($stream)) {
153
                fclose($stream);
154
            }
155
        }
156
    }
157
158
    /**
159
     * Creates a directory (no-op if it already exists).
160
     * Equivalent to mkdir().
161
     *
162
     * @throws FilesystemException
163
     */
164
    public function createDirectory(string $path): void
165
    {
166
        $path = $this->normalize($path);
167
        $this->resourceFilesystem->createDirectory($path);
168
    }
169
170
    /**
171
     * Checks if a directory exists.
172
     *
173
     * @throws FilesystemException
174
     */
175
    public function directoryExists(string $path): bool
176
    {
177
        $path = $this->normalize($path);
178
        return $this->resourceFilesystem->directoryExists($path);
179
    }
180
181
    /**
182
     * Ensures a directory exists (creates if missing).
183
     *
184
     * @throws FilesystemException
185
     */
186
    public function ensureDirectory(string $path): void
187
    {
188
        if (!$this->directoryExists($path)) {
189
            $this->createDirectory($path);
190
        }
191
    }
192
193
    /**
194
     * Lists files and directories under a given path as a flat array.
195
     * Equivalent to scandir(), opendir(), readdir().
196
     *
197
     * @param string $path Path to list (empty string = root).
198
     * @param bool   $deep true = recursive listing, false = shallow listing.
199
     * @return array<int, \League\Flysystem\StorageAttributes>
200
     * @throws FilesystemException
201
     */
202
    public function listContents(string $path = '', bool $deep = false): array
203
    {
204
        $path = $this->normalize($path);
205
        return $this->resourceFilesystem
206
            ->listContents($path, $deep)
207
            ->toArray();
208
    }
209
210
    /**
211
     * Gets the size of a file in bytes.
212
     * Equivalent to filesize().
213
     *
214
     * @throws FilesystemException
215
     */
216
    public function fileSize(string $path): int
217
    {
218
        $path = $this->normalize($path);
219
        return $this->resourceFilesystem->fileSize($path);
220
    }
221
222
    /**
223
     * Gets the last modified timestamp of a file.
224
     * Equivalent to filemtime().
225
     *
226
     * @throws FilesystemException
227
     */
228
    public function lastModified(string $path): int
229
    {
230
        $path = $this->normalize($path);
231
        return $this->resourceFilesystem->lastModified($path);
232
    }
233
234
    /**
235
     * Deletes an entire directory and its contents.
236
     * Equivalent to rmdir() or a recursive delete.
237
     *
238
     * @throws FilesystemException
239
     */
240
    public function deleteDirectory(string $path): void
241
    {
242
        $path = $this->normalize($path);
243
        $this->resourceFilesystem->deleteDirectory($path);
244
    }
245
246
    /**
247
     * Copies a file to a new location.
248
     * Equivalent to copy().
249
     *
250
     * @throws FilesystemException
251
     */
252
    public function copy(string $source, string $destination): void
253
    {
254
        $source = $this->normalize($source);
255
        $destination = $this->normalize($destination);
256
        $this->resourceFilesystem->copy($source, $destination);
257
    }
258
259
    /**
260
     * Moves or renames a file.
261
     * Equivalent to rename() or a file move.
262
     *
263
     * @throws FilesystemException
264
     */
265
    public function move(string $source, string $destination): void
266
    {
267
        $source = $this->normalize($source);
268
        $destination = $this->normalize($destination);
269
        $this->resourceFilesystem->move($source, $destination);
270
    }
271
272
    /**
273
     * Returns the MIME type if the adapter supports it, null otherwise.
274
     */
275
    public function mimeType(string $path): ?string
276
    {
277
        $path = $this->normalize($path);
278
        try {
279
            return $this->resourceFilesystem->mimeType($path);
280
        } catch (\Throwable) {
281
            return null;
282
        }
283
    }
284
285
    /**
286
     * Moves an uploaded file (local temp) into Flysystem storage.
287
     * Equivalent to UploadedFile::move() semantics (write + delete original).
288
     *
289
     * @param string $tempPath   Local temporary path of the uploaded file.
290
     * @param string $targetPath Target path in the filesystem.
291
     * @throws FilesystemException
292
     */
293
    public function moveUploadedFile(string $tempPath, string $targetPath): void
294
    {
295
        $targetPath = $this->normalize($targetPath);
296
297
        $stream = @fopen($tempPath, 'r');
298
        if (!\is_resource($stream)) {
299
            throw new \RuntimeException("Cannot open temporary upload: {$tempPath}");
300
        }
301
302
        try {
303
            $this->writeStream($targetPath, $stream);
304
        } finally {
305
            if (\is_resource($stream)) {
306
                fclose($stream);
307
            }
308
        }
309
310
        if (is_file($tempPath)) {
311
            @unlink($tempPath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

311
            /** @scrutinizer ignore-unhandled */ @unlink($tempPath);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
312
        }
313
    }
314
}
315