Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.24% covered (success)
88.24%
45 / 51
66.67% covered (warning)
66.67%
6 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
UploadedFile
88.24% covered (success)
88.24%
45 / 51
66.67% covered (warning)
66.67%
6 / 9
22.79
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getStream
87.50% covered (success)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 moveTo
66.67% covered (warning)
66.67%
8 / 12
0.00% covered (danger)
0.00%
0 / 1
8.81
 getSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getError
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getClientFilename
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getClientMediaType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromFile
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 createFromFilesArray
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
5.01
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Http\Server;
16
17use Psr\Http\Message\StreamInterface;
18use Psr\Http\Message\UploadedFileInterface;
19use Pop\Http\Body;
20
21/**
22 * HTTP server uploaded file class
23 *
24 * @category   Pop
25 * @package    Pop\Http
26 * @author     Nick Sagona, III <nick@popphp.org>
27 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
28 * @license    https://www.popphp.org/license     New BSD License
29 * @version    6.0.0
30 */
31class UploadedFile implements UploadedFileInterface
32{
33
34    /**
35     * Path to the underlying (typically temporary) uploaded file
36     * @var string
37     */
38    protected string $file;
39
40    /**
41     * File size in bytes
42     * @var int
43     */
44    protected int $size;
45
46    /**
47     * One of PHP's UPLOAD_ERR_* constants
48     * @var int
49     */
50    protected int $error;
51
52    /**
53     * Client-reported original filename
54     * @var ?string
55     */
56    protected ?string $clientFilename;
57
58    /**
59     * Client-reported media type
60     * @var ?string
61     */
62    protected ?string $clientMediaType;
63
64    /**
65     * Lazily-built stream wrapping $file
66     * @var ?Body
67     */
68    protected ?Body $stream = null;
69
70    /**
71     * Whether moveTo() has already been called
72     * @var bool
73     */
74    protected bool $moved = false;
75
76    /**
77     * Constructor
78     *
79     * @param string  $file
80     * @param int     $size
81     * @param int     $error
82     * @param ?string $clientFilename
83     * @param ?string $clientMediaType
84     */
85    public function __construct(string $file, int $size, int $error, ?string $clientFilename = null, ?string $clientMediaType = null)
86    {
87        $this->file            = $file;
88        $this->size            = $size;
89        $this->error           = $error;
90        $this->clientFilename  = $clientFilename;
91        $this->clientMediaType = $clientMediaType;
92    }
93
94    /**
95     * Get a stream wrapping the uploaded file's contents
96     *
97     * @throws Exception
98     * @return StreamInterface
99     */
100    public function getStream(): StreamInterface
101    {
102        if ($this->error !== UPLOAD_ERR_OK) {
103            throw new Exception('Error: Cannot retrieve the stream - the upload failed with error code ' . $this->error . '.');
104        }
105        if ($this->moved) {
106            throw new Exception('Error: Cannot retrieve the stream - the file has already been moved.');
107        }
108
109        if ($this->stream === null) {
110            $this->stream = new Body();
111            $this->stream->setContentFromFile($this->file);
112        }
113
114        return $this->stream;
115    }
116
117    /**
118     * Move the uploaded file to a new location
119     *
120     * @param  string $targetPath
121     * @param  bool   $secure
122     * @throws Exception
123     * @return void
124     */
125    public function moveTo(string $targetPath, bool $secure = false): void
126    {
127        if ($this->error !== UPLOAD_ERR_OK) {
128            throw new Exception('Error: Cannot move the file - the upload failed with error code ' . $this->error . '.');
129        }
130        if ($this->moved) {
131            throw new Exception('Error: Cannot move the file - it has already been moved.');
132        }
133
134        if (is_uploaded_file($this->file)) {
135            if (!move_uploaded_file($this->file, $targetPath)) {
136                throw new Exception("Error: Unable to move the uploaded file to '" . $targetPath . "'.");
137            }
138        } else if ($secure) {
139            throw new Exception("Error: Unable to move the file to '" . $targetPath . "' - not a genuine upload.");
140        } else if (!rename($this->file, $targetPath)) {
141            throw new Exception("Error: Unable to move the file to '" . $targetPath . "'.");
142        }
143
144        $this->moved = true;
145    }
146
147    /**
148     * Get the file size in bytes
149     *
150     * @return ?int
151     */
152    public function getSize(): ?int
153    {
154        return $this->size;
155    }
156
157    /**
158     * Get the upload error code (one of PHP's UPLOAD_ERR_* constants)
159     *
160     * @return int
161     */
162    public function getError(): int
163    {
164        return $this->error;
165    }
166
167    /**
168     * Get the client-reported original filename
169     *
170     * @return ?string
171     */
172    public function getClientFilename(): ?string
173    {
174        return $this->clientFilename;
175    }
176
177    /**
178     * Get the client-reported media type
179     *
180     * @return ?string
181     */
182    public function getClientMediaType(): ?string
183    {
184        return $this->clientMediaType;
185    }
186
187    /**
188     * Build a single UploadedFile instance from one $_FILES-shaped entry
189     *
190     * @param  array $file
191     * @return UploadedFile
192     */
193    public static function fromFile(array $file): self
194    {
195        return new self(
196            $file['tmp_name'],
197            (int)($file['size'] ?? 0),
198            (int)($file['error'] ?? UPLOAD_ERR_OK),
199            $file['name'] ?? null,
200            $file['type'] ?? null
201        );
202    }
203
204    /**
205     * Normalize a native $_FILES-shaped array (flat or per-field multi-file) into
206     * an array of UploadedFile instances, preserving the original field-name keys
207     *
208     * @param  array $files
209     * @return array
210     */
211    public static function createFromFilesArray(array $files): array
212    {
213        $uploadedFiles = [];
214
215        foreach ($files as $name => $file) {
216            if (!isset($file['tmp_name'])) {
217                continue;
218            }
219
220            if (is_array($file['tmp_name'])) {
221                foreach (array_keys($file['tmp_name']) as $key) {
222                    $uploadedFiles[$name][$key] = new self(
223                        $file['tmp_name'][$key],
224                        (int)($file['size'][$key] ?? 0),
225                        (int)($file['error'][$key] ?? UPLOAD_ERR_OK),
226                        $file['name'][$key] ?? null,
227                        $file['type'][$key] ?? null
228                    );
229                }
230            } else {
231                $uploadedFiles[$name] = self::fromFile($file);
232            }
233        }
234
235        return $uploadedFiles;
236    }
237
238}