Passed
Push — master ( 3a5683...386b65 )
by Terry
01:50
created

src/Psr7/UploadedFile.php (1 issue)

Severity
1
<?php 
2
/*
3
 * This file is part of the Shieldon package.
4
 *
5
 * (c) Terry L. <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
declare(strict_types=1);
12
13
namespace Shieldon\Psr7;
14
15
use Psr\Http\Message\StreamInterface;
16
use Psr\Http\Message\UploadedFileInterface;
17
use InvalidArgumentException;
18
use RuntimeException;
19
20
use function file_exists;
21
use function file_put_contents;
22
use function is_string;
23
use function is_uploaded_file;
24
use function is_writable;
25
use function move_uploaded_file;
26
use function php_sapi_name;
27
use function rename;
28
29
use const UPLOAD_ERR_CANT_WRITE;
30
use const UPLOAD_ERR_EXTENSION;
31
use const UPLOAD_ERR_FORM_SIZE;
32
use const UPLOAD_ERR_INI_SIZE;
33
use const UPLOAD_ERR_NO_FILE;
34
use const UPLOAD_ERR_NO_TMP_DIR;
35
use const UPLOAD_ERR_OK;
36
use const UPLOAD_ERR_PARTIAL;
37
use const LOCK_EX;
38
39
/*
40
 * Describes a data stream.
41
 */
42
class UploadedFile implements UploadedFileInterface
43
{
44
    /**
45
     * The full path of the file provided by client.
46
     *
47
     * @var string|null
48
     */
49
    protected $file;
50
51
    /**
52
     * A stream representing the uploaded file.
53
     *
54
     * @var StreamInterface|null
55
     */
56
    protected $stream;
57
58
    /**
59
     * The file size based on the "size" key in the $_FILES array.
60
     *
61
     * @var int|null
62
     */
63
    protected $size;
64
65
    /**
66
     * The filename based on the "name" key in the $_FILES array.
67
     *
68
     * @var string|null
69
     */
70
    protected $name;
71
72
    /**
73
     * The type of a file. This value is based on the "type" key in the $_FILES array.
74
     *
75
     * @var string|null
76
     */
77
    protected $type;
78
79
    /**
80
     * The error code associated with the uploaded file.
81
     *
82
     * @var int
83
     */
84
    protected $error;
85
86
    /**
87
     * Check if the uploaded file has been moved or not.
88
     *
89
     * @var bool
90
     */
91
    protected $isMoved = false;
92
93
    /**
94
     * The type of interface between web server and PHP.
95
     * This value is typically from `php_sapi_name`, might be changed ony for
96
     * unit testing purpose.
97
     *
98
     * @var string
99
     */
100
    private $sapi;
101
102
    /**
103
     * UploadedFile constructor.
104
     * 
105
     * @param string|StreamInterface $source The full path of a file or stream.
106
     * @param string|null            $name   The file name.
107
     * @param string|null            $type   The file media type.
108
     * @param int|null               $size   The file size in bytes.
109
     * @param int                    $error  The status code of the upload.
110
     * @param string|null            $sapi   Only assign for unit testing purpose.
111
     */
112
    public function __construct(
113
                $source       ,
114
        ?string $name   = null,
115
        ?string $type   = null,
116
        ?int    $size   = null,
117
        int     $error  = 0   ,
118
        ?string $sapi   = null
119
    ) {
120
121
        if (is_string($source)) {
122
            $this->file = $source;
123
124
        } elseif ($source instanceof StreamInterface) {
0 ignored issues
show
$source is always a sub-type of Psr\Http\Message\StreamInterface.
Loading history...
125
            $this->stream = $source;
126
127
        } else {
128
            throw new InvalidArgumentException(
129
                'First argument accepts only a string or StreamInterface instance.'
130
            );
131
        }
132
133
        $this->name  = $name;
134
        $this->type  = $type;
135
        $this->size  = $size;
136
        $this->error = $error;
137
        $this->sapi  = php_sapi_name();
138
139
        if ($sapi) {
140
            $this->sapi = $sapi;
141
        }
142
    }
143
144
    /**
145
     * {@inheritdoc}
146
     */
147
    public function getStream(): StreamInterface
148
    {
149
        if ($this->isMoved) {
150
            throw new RuntimeException(
151
                'The stream has been moved.'
152
            );
153
        }
154
155
        if (! $this->stream) {
156
            throw new RuntimeException(
157
                'No stream is available or can be created.'
158
            );
159
        }
160
161
        return $this->stream;
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167
    public function moveTo($targetPath): void
168
    {
169
        if ($this->isMoved) {
170
            // Throw exception on the second or subsequent call to the method.
171
            throw new RuntimeException(
172
                'Uploaded file already moved'
173
            );
174
        }
175
176
        if (! is_writable(dirname($targetPath))) {
177
            // Throw exception if the $targetPath specified is invalid.
178
            throw new RuntimeException(
179
                sprintf(
180
                    'The target path "%s" is not writable.',
181
                    $targetPath
182
                )
183
            );
184
        }
185
186
        // Is a file..
187
        if (is_string($this->file) && ! empty($this->file)) {
188
189
            if ($this->sapi === 'cli') {
190
191
                if (! rename($this->file, $targetPath)) {
192
193
                    // @codeCoverageIgnoreStart
194
195
                    // Throw exception on any error during the move operation.
196
                    throw new RuntimeException(
197
                        sprintf(
198
                            'Could not rename the file to the target path "%s".',
199
                            $targetPath
200
                        )
201
                    );
202
203
                    // @codeCoverageIgnoreEnd
204
                }
205
            } else {
206
207
                if (
208
                    ! is_uploaded_file($this->file) || 
209
                    ! move_uploaded_file($this->file, $targetPath)
210
                ) {
211
                    // Throw exception on any error during the move operation.
212
                    throw new RuntimeException(
213
                        sprintf(
214
                            'Could not move the file to the target path "%s".',
215
                            $targetPath
216
                        )
217
                    );
218
                }
219
            }
220
        }
221
222
        // Is a stream...
223
        if ($this->stream instanceof StreamInterface) {
224
            $content = $this->stream->getContents();
225
226
            file_put_contents($targetPath, $content, LOCK_EX);
227
228
            // @codeCoverageIgnoreStart
229
230
            if (! file_exists($targetPath)) {
231
                // Throw exception on any error during the move operation.
232
                throw new RuntimeException(
233
                    sprintf(
234
                        'Could not move the stream to the target path "%s".',
235
                        $targetPath
236
                    )
237
                );
238
            }
239
240
            // @codeCoverageIgnoreEnd
241
242
            unset($content, $this->stream);
243
        }
244
245
        $this->isMoved = true;
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251
    public function getSize(): ?int
252
    {
253
        return $this->size;
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259
    public function getError(): int
260
    {
261
        return $this->error;
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     */
267
    public function getClientFilename(): ?string
268
    {
269
        return $this->name;
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275
    public function getClientMediaType(): ?string
276
    {
277
        return $this->type;
278
    }
279
280
    /*
281
    |--------------------------------------------------------------------------
282
    | Non-PSR-7 Methods.
283
    |--------------------------------------------------------------------------
284
    */
285
286
    /**
287
     * Get error message when uploading files.
288
     *
289
     * @return string
290
     */
291
    public function getErrorMessage(): string
292
    {
293
        $message = [
294
            UPLOAD_ERR_INI_SIZE   => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
295
            UPLOAD_ERR_FORM_SIZE  => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.',
296
            UPLOAD_ERR_PARTIAL    => 'The uploaded file was only partially uploaded.',
297
            UPLOAD_ERR_NO_FILE    => 'No file was uploaded.',
298
            UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.',
299
            UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
300
            UPLOAD_ERR_EXTENSION  => 'File upload stopped by extension.',
301
            UPLOAD_ERR_OK         => 'There is no error, the file uploaded with success.',
302
        ];
303
304
        return $message[$this->error] ?? 'Unknown upload error.';
305
    }
306
}
307