Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.50% covered (success)
92.50%
37 / 40
66.67% covered (warning)
66.67%
6 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractFile
92.50% covered (success)
92.50%
37 / 40
66.67% covered (warning)
66.67%
6 / 9
25.26
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 __destruct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 eof
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getChunk
n/a
0 / 0
n/a
0 / 0
0
 putChunk
n/a
0 / 0
n/a
0 / 0
0
 delete
83.33% covered (success)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
5.12
 getMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMode
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 copyToSimple
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 isOk
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deleteDir
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
9.02
1<?php
2
3/**
4 * File handler class.
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    Anatoliy Belsky <ab@php.net>
12 * @copyright 2009-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     2009-08-21
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Attachment\Filesystem;
21
22use phpMyFAQ\Attachment\Filesystem\File\FileException;
23use RecursiveDirectoryIterator;
24use RecursiveIteratorIterator;
25
26/**
27 * Class File.
28 *
29 * @package   phpMyFAQ
30 * @author    Anatoliy Belsky <ab@php.net>
31 * @copyright 2009-2026 phpMyFAQ Team
32 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
33 * @link      https://www.phpmyfaq.de
34 * @since     2009-08-21
35 */
36abstract class AbstractFile extends AbstractEntry
37{
38    public const string MODE_READ = 'rb';
39
40    public const string MODE_APPEND = 'ab';
41
42    public const string MODE_WRITE = 'wb';
43
44    /**
45     * Constructor.
46     *
47     * @param string $filepath path to file
48     * @param string $mode     mode for fopen
49     * @throws FileException
50     */
51    public function __construct(
52        string $filepath,
53        protected string $mode = self::MODE_READ,
54    ) {
55        $this->path = $filepath;
56
57        $this->handle = fopen($this->path, $this->mode);
58
59        if (!is_resource($this->handle)) {
60            throw new FileException('Could not open file: ' . $this->path);
61        }
62    }
63
64    /**
65     * Destructor.
66     */
67    public function __destruct()
68    {
69        if (is_resource($this->handle)) {
70            fclose($this->handle);
71        }
72    }
73
74    /**
75     * Either EOF was reached.
76     */
77    public function eof(): bool
78    {
79        return feof($this->handle);
80    }
81
82    /**
83     * Get the next file chunk.
84     */
85    abstract public function getChunk(): string;
86
87    /**
88     * Put chunk into file.
89     *
90     * @param string $chunk chunk to write
91     * @return int|bool bytes written or false
92     */
93    abstract public function putChunk(string $chunk): int|bool;
94
95    /**
96     * Deletes the file.
97     *
98     * @throws FileException
99     */
100    public function delete(): bool
101    {
102        if ($this->handle) {
103            fclose($this->handle);
104        }
105
106        // Read the uploaded temp path directly from $_FILES instead of
107        // rebuilding a Symfony Request. Request::createFromGlobals() eagerly
108        // constructs an UploadedFile with path validation, which throws a
109        // FileNotFoundException here because move_uploaded_file() has already
110        // moved the temp file away during the preceding copyTo() call.
111        $uploadedTmpName = $_FILES['userfile']['tmp_name'] ?? null;
112        if (is_string($uploadedTmpName) && $this->path !== $uploadedTmpName && file_exists($this->path)) {
113            return $this->deleteDir(dirname($this->path));
114        }
115
116        return true;
117    }
118
119    /**
120     * Return current file mode.
121     */
122    public function getMode(): string
123    {
124        return $this->mode;
125    }
126
127    /**
128     * Reopen a file in given mode.
129     *
130     * @param string $mode file mode
131     */
132    public function setMode($mode): bool
133    {
134        $retval = false;
135
136        if (in_array($mode, [self::MODE_WRITE, self::MODE_READ, self::MODE_APPEND], strict: true)) {
137            fclose($this->handle);
138            $this->handle = fopen($this->path, $mode);
139
140            $retval = is_resource($this->handle);
141        }
142
143        return $retval;
144    }
145
146    /**
147     * Simple copy file.
148     *
149     * @param string $target filepath
150     */
151    public function copyToSimple($target): bool
152    {
153        if (is_uploaded_file($this->path)) {
154            return move_uploaded_file($this->path, $target);
155        }
156
157        return copy($this->path, $target);
158    }
159
160    /**
161     * Self check.
162     */
163    public function isOk(): bool
164    {
165        return is_resource($this->handle);
166    }
167
168    /**
169     * Recursive deletion of path and file.
170     *
171     * @param string $path
172     *
173     * @throws FileException
174     */
175    public function deleteDir($path): bool
176    {
177        if (!file_exists($path)) {
178            throw new FileException(sprintf("Directory %s doesn't exist.", $path));
179        }
180
181        $it = new RecursiveIteratorIterator(
182            new RecursiveDirectoryIterator($path),
183            RecursiveIteratorIterator::CHILD_FIRST,
184        );
185
186        foreach ($it as $file) {
187            if (!$file instanceof \SplFileInfo || in_array($file->getBasename(), ['.', '..'], strict: true)) {
188                continue;
189            }
190
191            if ($file->isDir()) {
192                rmdir($file->getPathname());
193            }
194
195            if ($file->isFile() || $file->isLink()) {
196                if (!is_writable($file->getPathname())) {
197                    throw new FileException("File can't be deleted.");
198                }
199
200                unlink($file->getPathname());
201            }
202        }
203
204        return rmdir($path);
205    }
206}