Code

< 40 %
40-60 %
> 60 %
1
<?php
2
3
/*
4
 * This file is part of the Koded package.
5
 *
6
 * (c) Mihail Binev <[email protected]>
7
 *
8
 * Please view the LICENSE distributed with this source code
9
 * for the full copyright and license information.
10
 *
11
 */
12
13
namespace Koded\Http;
14
15
use Koded\Exceptions\KodedException;
16
use Psr\Http\Message\{StreamInterface, UploadedFileInterface};
17
use function Koded\Stdlib\randomstring;
18
19
class UploadedFile implements UploadedFileInterface
20
{
21
    private mixed $file;
22
    private mixed $name;
23
    private ?string $type;
24
    private ?int $size;
25
    private int  $error;
26
    private bool $moved = false;
27
28
    public function __construct(array $uploadedFile)
29
    {
30
        $this->file  = $uploadedFile['tmp_name'] ?? null;
31
        $this->name  = $uploadedFile['name'] ?? randomstring(9);
32
        $this->size  = $uploadedFile['size'] ?? null;
33
        $this->prepareFile();
34
        $this->type = $this->getClientMediaType();
35
        $this->error = (int)($uploadedFile['error'] ?? \UPLOAD_ERR_OK);
36
    }
37
38
    public function getStream(): StreamInterface
39
    {
40
        if ($this->moved) {
41
            throw UploadedFileException::streamNotAvailable();
42
        }
43 36
        return new FileStream($this->file, 'w+b');
44
    }
45 36
46 36
    public function moveTo($targetPath)
47 36
    {
48 36
        $this->assertUploadError();
49
        $this->assertTargetPath($targetPath);
50
        // @codeCoverageIgnoreStart
51 36
        try {
52 1
            $this->moved = ('cli' === \php_sapi_name())
53 1
                ? \rename($this->file, $targetPath)
54 1
                : \move_uploaded_file($this->file, $targetPath);
55 35
56 8
            @\unlink($this->file);
57 34
        } catch (\Throwable $e) {
58 1
            throw new \RuntimeException($e->getMessage());
59
        }
60
        // @codeCoverageIgnoreEnd
61
    }
62 35
63 35
    public function getSize(): ?int
64
    {
65
        return $this->size;
66 4
    }
67
68 4
    public function getError(): int
69 2
    {
70
        return $this->error;
71
    }
72 2
73
    public function getClientFilename(): ?string
74
    {
75
        return $this->name;
76 14
    }
77
78 14
    public function getClientMediaType(): ?string
79 13
    {
80
        try {
81
            return @(new \finfo(\FILEINFO_MIME_TYPE))->file($this->file);
82
        } catch (\Throwable) {
83
            return $this->type;
84
        }
85
    }
86
87
    private function assertUploadError(): void
88
    {
89
        if ($this->error !== \UPLOAD_ERR_OK) {
90
            throw new UploadedFileException($this->error);
91
        }
92 6
    }
93
94
    private function assertTargetPath($targetPath): void
95 2
    {
96
        if ($this->moved) {
97 2
            throw UploadedFileException::fileAlreadyMoved();
98
        }
99
        if (false === \is_string($targetPath) || 0 === \mb_strlen($targetPath)) {
100
            throw UploadedFileException::targetPathIsInvalid();
101 2
        }
102
        if (false === \is_dir($dirname = \dirname($targetPath))) {
103 2
            @\mkdir($dirname, 0777, true);
104
        }
105
    }
106
107 2
    private function prepareFile(): void
108
    {
109 2
        if ($this->file instanceof StreamInterface) {
110
            // Create a temporary file out of the stream object
111
            $this->size = $this->file->getSize();
112
            $file = \sys_get_temp_dir() . '/' . $this->name;
113 35
            \file_put_contents($file, $this->file->getContents());
114
            $this->file = $file;
115
            return;
116 35
        }
117 1
        if (false === \is_string($this->file)) {
118 1
            throw UploadedFileException::fileNotSupported($this->file);
119
        }
120
        if (0 === \mb_strlen($this->file)) {
121
            throw UploadedFileException::filenameCannotBeEmpty();
122
        }
123 14
    }
124
}
125 14
126 1
127
class UploadedFileException extends KodedException
128 13
{
129
    protected array $messages = [
130
        \UPLOAD_ERR_INI_SIZE   => 'The uploaded file exceeds the "upload_max_filesize" directive in php.ini',
131 13
        \UPLOAD_ERR_FORM_SIZE  => 'The uploaded file exceeds the "MAX_FILE_SIZE" directive that was specified in the HTML form',
132
        \UPLOAD_ERR_PARTIAL    => 'The uploaded file was only partially uploaded',
133 13
        \UPLOAD_ERR_NO_FILE    => 'No file was uploaded',
134 2
        \UPLOAD_ERR_NO_TMP_DIR => 'The temporary directory to write to is missing',
135
        \UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
136
        \UPLOAD_ERR_EXTENSION  => 'A PHP extension stopped the file upload',
137 13
    ];
138 7
139
    public static function streamNotAvailable(): \RuntimeException
140
    {
141 6
        return new \RuntimeException('Stream is not available, because the file was previously moved');
142 1
    }
143
144 6
    public static function targetPathIsInvalid(): \InvalidArgumentException
145
    {
146
        return new \InvalidArgumentException('The provided path for moveTo operation is not valid');
147
    }
148
149
    public static function fileAlreadyMoved(): \RuntimeException
150
    {
151
        return new \RuntimeException('File is not available, because it was previously moved');
152
    }
153
154
    public static function fileNotSupported(mixed $file): \InvalidArgumentException
155
    {
156
        return new \InvalidArgumentException(sprintf(
157
            'The uploaded file is not supported, expected string, %s given', \get_debug_type($file)
158
        ));
159
    }
160 2
161
    public static function filenameCannotBeEmpty(): \InvalidArgumentException
162 2
    {
163
        return new \InvalidArgumentException('Filename cannot be empty');
164
    }
165
}
166