Issues (29)

src/Psr7/UploadedFile.php (3 issues)

Labels
Severity
1
<?php
2
/**
3
 * Class UploadedFile
4
 *
5
 * @created      11.08.2018
6
 * @author       smiley <[email protected]>
7
 * @copyright    2018 smiley
8
 * @license      MIT
9
 */
10
11
namespace chillerlan\HTTP\Psr7;
12
13
use chillerlan\HTTP\Psr17\{FactoryHelpers, StreamFactory};
14
use Psr\Http\Message\{StreamInterface, UploadedFileInterface};
15
use InvalidArgumentException, RuntimeException;
16
17
use function in_array, is_file, is_string, is_writable, move_uploaded_file, php_sapi_name,rename;
18
19
use const UPLOAD_ERR_CANT_WRITE, UPLOAD_ERR_EXTENSION, UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_INI_SIZE,
20
	UPLOAD_ERR_NO_FILE, UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_OK, UPLOAD_ERR_PARTIAL;
21
22
class UploadedFile implements UploadedFileInterface{
23
24
	/** @var int[] */
25
	public const UPLOAD_ERRORS = [
26
		UPLOAD_ERR_OK,
27
		UPLOAD_ERR_INI_SIZE,
28
		UPLOAD_ERR_FORM_SIZE,
29
		UPLOAD_ERR_PARTIAL,
30
		UPLOAD_ERR_NO_FILE,
31
		UPLOAD_ERR_NO_TMP_DIR,
32
		UPLOAD_ERR_CANT_WRITE,
33
		UPLOAD_ERR_EXTENSION,
34
	];
35
36
	protected int              $error;
37
	protected int              $size;
38
	protected ?string          $clientFilename;
39
	protected ?string          $clientMediaType;
40
	protected ?string          $file            = null;
41
	protected ?StreamInterface $stream;
42
	protected bool             $moved           = false;
43
	protected StreamFactory    $streamFactory;
44
45
	/**
46
	 * @throws \InvalidArgumentException
47
	 */
48
	public function __construct(mixed $file, int $size, int $error = UPLOAD_ERR_OK, string $filename = null, string $mediaType = null){
49
50
		if(!in_array($error, $this::UPLOAD_ERRORS, true)){
51
			throw new InvalidArgumentException('Invalid error status for UploadedFile');
52
		}
53
54
		$this->size            = (int)$size; // int type hint also accepts float...
55
		$this->error           = $error;
56
		$this->clientFilename  = $filename;
57
		$this->clientMediaType = $mediaType;
58
		$this->streamFactory   = new StreamFactory;
59
60
		if($this->error === UPLOAD_ERR_OK){
61
62
			if(is_string($file)){
63
				$this->file = $file;
64
			}
65
			else{
66
				$this->stream = FactoryHelpers::create_stream_from_input($file);
67
			}
68
69
		}
70
71
	}
72
73
	/**
74
	 * @inheritDoc
75
	 */
76
	public function getStream():StreamInterface{
77
78
		$this->validateActive();
79
80
		if($this->stream instanceof StreamInterface){
81
			return $this->stream;
82
		}
83
84
		if(is_file($this->file)){
0 ignored issues
show
It seems like $this->file can also be of type null; however, parameter $filename of is_file() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

84
		if(is_file(/** @scrutinizer ignore-type */ $this->file)){
Loading history...
85
			return $this->streamFactory->createStreamFromFile($this->file, 'r+');
0 ignored issues
show
It seems like $this->file can also be of type null; however, parameter $filename of chillerlan\HTTP\Psr17\St...:createStreamFromFile() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

85
			return $this->streamFactory->createStreamFromFile(/** @scrutinizer ignore-type */ $this->file, 'r+');
Loading history...
86
		}
87
88
		return $this->streamFactory->createStream($this->file);
0 ignored issues
show
It seems like $this->file can also be of type null; however, parameter $content of chillerlan\HTTP\Psr17\St...Factory::createStream() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
		return $this->streamFactory->createStream(/** @scrutinizer ignore-type */ $this->file);
Loading history...
89
	}
90
91
	/**
92
	 * @inheritDoc
93
	 */
94
	public function moveTo($targetPath):void{
95
96
		$this->validateActive();
97
98
		if(is_string($targetPath) && empty($targetPath)){
99
			throw new InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
100
		}
101
102
		if(!is_writable($targetPath)){
103
			throw new RuntimeException('Directory is not writable: '.$targetPath);
104
		}
105
106
		if($this->file !== null){
107
			$this->moved = php_sapi_name() === 'cli'
108
				? rename($this->file, $targetPath)
109
				: move_uploaded_file($this->file, $targetPath);
110
		}
111
		else{
112
			$this->copyToStream($this->streamFactory->createStreamFromFile($targetPath, 'r+'));
113
			$this->moved = true;
114
		}
115
116
		if($this->moved === false){
117
			throw new RuntimeException('Uploaded file could not be moved to '.$targetPath); // @codeCoverageIgnore
118
		}
119
120
	}
121
122
	/**
123
	 * @inheritDoc
124
	 */
125
	public function getSize():?int{
126
		return $this->size;
127
	}
128
129
	/**
130
	 * @inheritDoc
131
	 */
132
	public function getError():int{
133
		return $this->error;
134
	}
135
136
	/**
137
	 * @inheritDoc
138
	 */
139
	public function getClientFilename():?string{
140
		return $this->clientFilename;
141
	}
142
143
	/**
144
	 * @inheritDoc
145
	 */
146
	public function getClientMediaType():?string{
147
		return $this->clientMediaType;
148
	}
149
150
	/**
151
	 * @throws RuntimeException if is moved or not ok
152
	 */
153
	protected function validateActive():void{
154
155
		if($this->error !== UPLOAD_ERR_OK){
156
			throw new RuntimeException('Cannot retrieve stream due to upload error');
157
		}
158
159
		if($this->moved){
160
			throw new RuntimeException('Cannot retrieve stream after it has already been moved');
161
		}
162
163
	}
164
165
	/**
166
	 * Copy the contents of a stream into another stream until the given number
167
	 * of bytes have been read.
168
	 *
169
	 * @author Michael Dowling and contributors to guzzlehttp/psr7
170
	 *
171
	 * @throws \RuntimeException on error
172
	 */
173
	protected function copyToStream(StreamInterface $dest):void{
174
		$source = $this->getStream();
175
176
		if($source->isSeekable()){
177
			$source->rewind();
178
		}
179
180
		while(!$source->eof()){
181
182
			if(!$dest->write($source->read(1048576))){
183
				break; // @codeCoverageIgnore
184
			}
185
186
		}
187
188
	}
189
190
}
191