Issues (31)

src/UploadedFile.php (1 issue)

1
<?php declare(strict_types=1);
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Nekhay <[email protected]>
7
 * @copyright Copyright (c) 2018, Anatoly Nekhay
8
 * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/http-message
10
 */
11
12
namespace Sunrise\Http\Message;
13
14
use Psr\Http\Message\StreamInterface;
15
use Psr\Http\Message\UploadedFileInterface;
16
use Sunrise\Http\Message\Exception\RuntimeException;
17
use Throwable;
18
use TypeError;
19
20
use function dirname;
21
use function gettype;
22
use function is_dir;
23
use function is_file;
24
use function is_readable;
25
use function is_string;
26
use function is_uploaded_file;
27
use function is_writable;
28
use function move_uploaded_file;
29
use function rename;
30
use function sprintf;
31
32
use const UPLOAD_ERR_OK;
33
use const UPLOAD_ERR_INI_SIZE;
34
use const UPLOAD_ERR_FORM_SIZE;
35
use const UPLOAD_ERR_PARTIAL;
36
use const UPLOAD_ERR_NO_FILE;
37
use const UPLOAD_ERR_NO_TMP_DIR;
38
use const UPLOAD_ERR_CANT_WRITE;
39
use const UPLOAD_ERR_EXTENSION;
40
41
/**
42
 * @psalm-suppress ClassMustBeFinal
43
 */
44
class UploadedFile implements UploadedFileInterface
45
{
46
    /**
47
     * @link https://www.php.net/manual/en/features.file-upload.errors.php
48
     *
49
     * @var array<int, non-empty-string>
50
     */
51
    public const UPLOAD_ERRORS = [
52
        UPLOAD_ERR_OK         => 'No error',
53
        UPLOAD_ERR_INI_SIZE   => 'Uploaded file exceeds the upload_max_filesize directive in the php.ini',
54
        UPLOAD_ERR_FORM_SIZE  => 'Uploaded file exceeds the MAX_FILE_SIZE directive in the HTML form',
55
        UPLOAD_ERR_PARTIAL    => 'Uploaded file was only partially uploaded',
56
        UPLOAD_ERR_NO_FILE    => 'No file was uploaded',
57
        UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary directory',
58
        UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
59
        UPLOAD_ERR_EXTENSION  => 'File upload was stopped by a PHP extension',
60
    ];
61
62
    private ?StreamInterface $stream;
63
    private ?int $size;
64
    private int $errorCode;
65
    private string $errorMessage;
66
    private ?string $clientFilename;
67
    private ?string $clientMediaType;
68
    private bool $isMoved = false;
69
70 40
    public function __construct(
71
        ?StreamInterface $stream,
72
        ?int $size = null,
73
        int $error = UPLOAD_ERR_OK,
74
        ?string $clientFilename = null,
75
        ?string $clientMediaType = null
76
    ) {
77 40
        $this->stream = $stream;
78 40
        $this->size = $size;
79 40
        $this->errorCode = $error;
80 40
        $this->errorMessage = self::UPLOAD_ERRORS[$error] ?? 'Unknown file upload error';
81 40
        $this->clientFilename = $clientFilename;
82 40
        $this->clientMediaType = $clientMediaType;
83
    }
84
85
    /**
86
     * @inheritDoc
87
     */
88 34
    public function getStream(): StreamInterface
89
    {
90 34
        if ($this->isMoved) {
91 4
            throw new RuntimeException('Uploaded file was moved');
92
        }
93
94 34
        if ($this->errorCode !== UPLOAD_ERR_OK) {
95 16
            throw new RuntimeException($this->errorMessage, $this->errorCode);
96
        }
97
98 18
        if ($this->stream === null) {
99 2
            throw new RuntimeException('Uploaded file has no stream');
100
        }
101
102 16
        return $this->stream;
103
    }
104
105
    /**
106
     * @inheritDoc
107
     */
108 19
    public function moveTo($targetPath): void
109
    {
110
        /** @psalm-suppress TypeDoesNotContainType */
111
        // @phpstan-ignore function.alreadyNarrowedType
112 19
        if (!is_string($targetPath)) {
113 1
            throw new TypeError(sprintf(
114 1
                'Argument #1 ($targetPath) must be of type string, %s given',
115 1
                gettype($targetPath),
116 1
            ));
117
        }
118
119 18
        $sourceStream = $this->getStream();
120
121 9
        $sourcePath = $sourceStream->getMetadata('uri');
122 9
        if (!is_string($sourcePath) || !is_file($sourcePath) || !is_readable($sourcePath)) {
123 1
            throw new RuntimeException('Uploaded file does not exist or is not readable');
124
        }
125
126 8
        $sourceDirname = dirname($sourcePath);
127 8
        if (!is_writable($sourceDirname)) {
128
            throw new RuntimeException('To move the uploaded file, the source directory must be writable');
129
        }
130
131 8
        $targetDirname = dirname($targetPath);
132 8
        if (!is_dir($targetDirname) || !is_writable($targetDirname)) {
133
            throw new RuntimeException('To move the uploaded file, the target directory must exist and be writable');
134
        }
135
136
        try {
137 8
            $this->isMoved = is_uploaded_file($sourcePath)
138
                ? move_uploaded_file($sourcePath, $targetPath)
139 8
                : rename($sourcePath, $targetPath);
140
        } catch (Throwable $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
141
        }
142
143 8
        if (!$this->isMoved) {
144
            throw new RuntimeException('Failed to move the uploaded file');
145
        }
146
147 8
        $sourceStream->close();
148 8
        $this->stream = null;
149
    }
150
151
    /**
152
     * @inheritDoc
153
     */
154 6
    public function getSize(): ?int
155
    {
156 6
        return $this->size;
157
    }
158
159
    /**
160
     * @inheritDoc
161
     */
162 7
    public function getError(): int
163
    {
164 7
        return $this->errorCode;
165
    }
166
167
    /**
168
     * @inheritDoc
169
     */
170 7
    public function getClientFilename(): ?string
171
    {
172 7
        return $this->clientFilename;
173
    }
174
175
    /**
176
     * @inheritDoc
177
     */
178 7
    public function getClientMediaType(): ?string
179
    {
180 7
        return $this->clientMediaType;
181
    }
182
}
183