Test Failed
Branch master (f83914)
by Rasul
07:02
created

UploadedFile   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 164
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 70
dl 0
loc 164
rs 9.92
c 0
b 0
f 0
wmc 31
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Furious\Psr7;
6
7
use Furious\Psr7\Exception\InvalidArgumentException;
8
use Furious\Psr7\Exception\InvalidUploadErrorException;
9
use Furious\Psr7\Exception\RuntimeException;
10
use Furious\Psr7\Exception\StreamAlreadyMovedException;
11
use Furious\Psr7\Exception\UploadErrorException;
12
use Psr\Http\Message\StreamInterface;
13
use Psr\Http\Message\UploadedFileInterface;
14
use function fopen;
15
use function is_resource;
16
use function is_string;
17
use function move_uploaded_file;
18
use function rename;
19
use function sprintf;
20
use const PHP_SAPI;
21
use const UPLOAD_ERR_CANT_WRITE;
22
use const UPLOAD_ERR_EXTENSION;
23
use const UPLOAD_ERR_FORM_SIZE;
24
use const UPLOAD_ERR_INI_SIZE;
25
use const UPLOAD_ERR_NO_FILE;
26
use const UPLOAD_ERR_NO_TMP_DIR;
27
use const UPLOAD_ERR_OK;
28
use const UPLOAD_ERR_PARTIAL;
29
30
class UploadedFile implements UploadedFileInterface
31
{
32
    private int $error;
1 ignored issue
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_STRING, expecting T_FUNCTION or T_CONST
Loading history...
33
    private ?int $size;
34
    private bool $moved = false;
35
    private ?string $file = null;
36
    private ?string $clientFileName = null;
37
    private ?string $clientMediaType = null;
38
    private ?StreamInterface $stream = null;
39
40
    private const ERROR_MESSAGES = [
41
        UPLOAD_ERR_OK         => 'The file successfully uploaded.',
42
        UPLOAD_ERR_INI_SIZE   => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
43
        UPLOAD_ERR_FORM_SIZE  => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.',
44
        UPLOAD_ERR_PARTIAL    => 'The uploaded file was only partially uploaded.',
45
        UPLOAD_ERR_NO_FILE    => 'No file was uploaded.',
46
        UPLOAD_ERR_NO_TMP_DIR => 'Missing a temp folder.',
47
        UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
48
        UPLOAD_ERR_EXTENSION  => 'A PHP extension stopped the file upload.'
49
    ];
50
51
    public function __construct(
52
        $streamOrFile, ?int $size, int $error,
53
        string $clientFileName = null, string $clientMediaType = null
54
    )
55
    {
56
        if ($error < 0 or $error > 8) {
57
            throw new InvalidUploadErrorException('Invalid error status for UploadedFile');
58
        }
59
60
        $this->error = $error;
61
62
        if (UPLOAD_ERR_OK !== $error) {
63
            throw new UploadErrorException(self::ERROR_MESSAGES[$this->error]);
64
        }
65
66
        $this->size = $size;
67
        $this->clientFileName = $clientFileName;
68
        $this->clientMediaType = $clientMediaType;
69
70
        if (UPLOAD_ERR_OK === $this->error) {
71
            $this->initializeStream($streamOrFile);
72
        }
73
    }
74
75
    private function initializeStream($streamOrFile): void
76
    {
77
        if (is_string($streamOrFile)) {
78
            $this->file = $streamOrFile;
79
        } elseif (is_resource($streamOrFile)) {
80
            $this->stream = Stream::new($streamOrFile);
81
        } elseif ($streamOrFile instanceof StreamInterface) {
82
            $this->stream = $streamOrFile;
83
        } else {
84
            throw new InvalidArgumentException('Invalid stream or file provided for UploadedFile');
85
        }
86
    }
87
88
    // Get
89
90
    public function getSize(): ?int
91
    {
92
        return $this->size;
93
    }
94
95
    public function getError(): int
96
    {
97
        return $this->error;
98
    }
99
100
    public function getClientFilename(): ?string
101
    {
102
        return $this->clientFileName;
103
    }
104
105
    public function getClientMediaType(): ?string
106
    {
107
        return $this->clientMediaType;
108
    }
109
110
    public function getStream(): StreamInterface
111
    {
112
        $this->validateActive();
113
114
        if ($this->stream instanceof StreamInterface) {
115
            return $this->stream;
116
        }
117
118
        $resource = fopen($this->file, 'r');
119
120
        return Stream::new($resource);
121
    }
122
123
    // Move
124
125
    public function moveTo($targetPath): void
126
    {
127
        $this->validateActive();
128
129
        if (!is_string($targetPath) or '' === $targetPath) {
130
            throw new InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
131
        }
132
133
        if (null !== $this->file) {
134
            $this->setIsMovedByFile($targetPath);
135
        } else {
136
            $stream = $this->getStream();
137
            if ($stream->isSeekable()) {
138
                $stream->rewind();
139
            }
140
141
            $this->copyStreamContent($stream, $targetPath);
142
            $this->moved = true;
143
        }
144
145
        $this->validateIsMoved($targetPath);
146
    }
147
148
    /**
149
     * @param bool $moved
150
     */
151
    public function setMoved(bool $moved): void
152
    {
153
        $this->moved = $moved;
154
    }
155
156
    // Validate
157
158
    private function validateActive(): void
159
    {
160
        if (UPLOAD_ERR_OK !== $this->error) {
161
            throw new UploadErrorException('Cannot retrieve stream due to upload error');
162
        }
163
164
        if ($this->moved) {
165
            throw new StreamAlreadyMovedException('Cannot retrieve stream after it has already been moved');
166
        }
167
    }
168
169
    private function validateIsMoved(string $targetPath): void
170
    {
171
        if (false === $this->moved) {
172
            throw new RuntimeException(sprintf('Uploaded file could not be moved to %s', $targetPath));
173
        }
174
    }
175
176
    // Copy
177
178
    private function copyStreamContent(StreamInterface $stream, string $targetPath): void
179
    {
180
        $dest = Stream::new(fopen($targetPath, 'w'));
181
        while (!$stream->eof()) {
182
            if (!$dest->write($stream->read(1048576))) {
183
                break;
184
            }
185
        }
186
    }
187
188
    private function setIsMovedByFile($targetPath): void
189
    {
190
        if ('cli' === PHP_SAPI) {
191
            $this->moved = rename($this->file, $targetPath);
192
        } else {
193
            $this->moved = move_uploaded_file($this->file, $targetPath);
194
        }
195
    }
196
}