kodedphp /
http
| 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 InvalidArgumentException; |
||||||
| 16 | use Koded\Exceptions\KodedException; |
||||||
| 17 | use Psr\Http\Message\{StreamInterface, UploadedFileInterface}; |
||||||
| 18 | use RuntimeException; |
||||||
| 19 | use Throwable; |
||||||
| 20 | use function Koded\Stdlib\randomstring; |
||||||
| 21 | |||||||
| 22 | |||||||
| 23 | class UploadedFile implements UploadedFileInterface |
||||||
| 24 | { |
||||||
| 25 | /** @var string|null */ |
||||||
| 26 | private $file; |
||||||
| 27 | |||||||
| 28 | /** @var string|null */ |
||||||
| 29 | private $name; |
||||||
| 30 | |||||||
| 31 | /** @var string|null */ |
||||||
| 32 | private $type; |
||||||
| 33 | |||||||
| 34 | /** @var int|null */ |
||||||
| 35 | private $size; |
||||||
| 36 | |||||||
| 37 | /** @var int See UPLOAD_ERR_* constants */ |
||||||
| 38 | private $error = \UPLOAD_ERR_OK; |
||||||
| 39 | |||||||
| 40 | /** @var bool */ |
||||||
| 41 | private $moved = false; |
||||||
| 42 | |||||||
| 43 | public function __construct(array $uploadedFile) |
||||||
| 44 | { |
||||||
| 45 | $this->size = $uploadedFile['size'] ?? null; |
||||||
| 46 | $this->file = $uploadedFile['tmp_name'] ?? null; |
||||||
| 47 | $this->name = $uploadedFile['name'] ?? randomstring(9); |
||||||
| 48 | $this->error = (int)($uploadedFile['error'] ?? \UPLOAD_ERR_OK); |
||||||
| 49 | |||||||
| 50 | // Create a file out of the stream |
||||||
| 51 | if ($this->file instanceof StreamInterface) { |
||||||
| 52 | $file = sys_get_temp_dir() . '/' . $this->name; |
||||||
| 53 | file_put_contents($file, $this->file->getContents()); |
||||||
| 54 | $this->file = $file; |
||||||
| 55 | } elseif (false === is_string($this->file)) { |
||||||
| 56 | throw UploadedFileException::fileNotSupported(); |
||||||
| 57 | } elseif (0 === strlen($this->file)) { |
||||||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||||
| 58 | throw UploadedFileException::filenameCannotBeEmpty(); |
||||||
| 59 | } |
||||||
| 60 | // Never trust the provided mime type |
||||||
| 61 | $this->type = $this->getClientMediaType(); |
||||||
| 62 | } |
||||||
| 63 | |||||||
| 64 | public function getStream(): StreamInterface |
||||||
| 65 | { |
||||||
| 66 | if ($this->moved) { |
||||||
| 67 | throw UploadedFileException::streamNotAvailable(); |
||||||
| 68 | } |
||||||
| 69 | return new FileStream($this->file, 'w+b'); |
||||||
|
0 ignored issues
–
show
It seems like
$this->file can also be of type null; however, parameter $filename of Koded\Http\FileStream::__construct() 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
Loading history...
|
|||||||
| 70 | } |
||||||
| 71 | |||||||
| 72 | public function moveTo($targetPath) |
||||||
| 73 | { |
||||||
| 74 | $this->assertUploadError(); |
||||||
| 75 | $this->assertTargetPath($targetPath); |
||||||
| 76 | // @codeCoverageIgnoreStart |
||||||
| 77 | try { |
||||||
| 78 | $this->moved = ('cli' === php_sapi_name()) |
||||||
| 79 | ? rename($this->file, $targetPath) |
||||||
|
0 ignored issues
–
show
It seems like
$this->file can also be of type null; however, parameter $from of rename() 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
Loading history...
|
|||||||
| 80 | : move_uploaded_file($this->file, $targetPath); |
||||||
|
0 ignored issues
–
show
It seems like
$this->file can also be of type null; however, parameter $from of move_uploaded_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
Loading history...
|
|||||||
| 81 | |||||||
| 82 | @unlink($this->file); |
||||||
|
0 ignored issues
–
show
It seems like
$this->file can also be of type null; however, parameter $filename of unlink() 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
Loading history...
It seems like you do not handle an error condition for
unlink(). This can introduce security issues, and is generally not recommended.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
If you suppress an error, we recommend checking for the error condition explicitly: // For example instead of
@mkdir($dir);
// Better use
if (@mkdir($dir) === false) {
throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
|
|||||||
| 83 | } catch (Throwable $e) { |
||||||
| 84 | throw new RuntimeException($e->getMessage()); |
||||||
| 85 | } |
||||||
| 86 | // @codeCoverageIgnoreEnd |
||||||
| 87 | } |
||||||
| 88 | |||||||
| 89 | public function getSize(): ?int |
||||||
| 90 | { |
||||||
| 91 | return $this->size; |
||||||
| 92 | } |
||||||
| 93 | |||||||
| 94 | public function getError(): int |
||||||
| 95 | { |
||||||
| 96 | return $this->error; |
||||||
| 97 | } |
||||||
| 98 | |||||||
| 99 | public function getClientFilename(): ?string |
||||||
| 100 | { |
||||||
| 101 | return $this->name; |
||||||
| 102 | } |
||||||
| 103 | |||||||
| 104 | public function getClientMediaType(): ?string |
||||||
| 105 | { |
||||||
| 106 | try { |
||||||
| 107 | return (new \finfo(\FILEINFO_MIME_TYPE))->file($this->file); |
||||||
| 108 | } catch (Throwable $e) { |
||||||
| 109 | return $this->type; |
||||||
| 110 | } |
||||||
| 111 | } |
||||||
| 112 | |||||||
| 113 | private function assertUploadError(): void |
||||||
| 114 | { |
||||||
| 115 | if ($this->error !== \UPLOAD_ERR_OK) { |
||||||
| 116 | throw new UploadedFileException($this->error); |
||||||
| 117 | } |
||||||
| 118 | } |
||||||
| 119 | |||||||
| 120 | private function assertTargetPath($targetPath): void |
||||||
| 121 | { |
||||||
| 122 | if ($this->moved) { |
||||||
| 123 | throw UploadedFileException::fileAlreadyMoved(); |
||||||
| 124 | } |
||||||
| 125 | if (false === is_string($targetPath) || 0 === strlen($targetPath)) { |
||||||
| 126 | throw UploadedFileException::targetPathIsInvalid(); |
||||||
| 127 | } |
||||||
| 128 | if (false === is_dir($dirname = dirname($targetPath))) { |
||||||
| 129 | @mkdir($dirname, 0777, true); |
||||||
|
0 ignored issues
–
show
It seems like you do not handle an error condition for
mkdir(). This can introduce security issues, and is generally not recommended.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
If you suppress an error, we recommend checking for the error condition explicitly: // For example instead of
@mkdir($dir);
// Better use
if (@mkdir($dir) === false) {
throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
|
|||||||
| 130 | } |
||||||
| 131 | } |
||||||
| 132 | } |
||||||
| 133 | |||||||
| 134 | |||||||
| 135 | class UploadedFileException extends KodedException |
||||||
| 136 | { |
||||||
| 137 | protected $messages = [ |
||||||
| 138 | \UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the "upload_max_filesize" directive in php.ini', |
||||||
| 139 | \UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the "MAX_FILE_SIZE" directive that was specified in the HTML form', |
||||||
| 140 | \UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded', |
||||||
| 141 | \UPLOAD_ERR_NO_FILE => 'No file was uploaded', |
||||||
| 142 | \UPLOAD_ERR_NO_TMP_DIR => 'The temporary directory to write to is missing', |
||||||
| 143 | \UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk', |
||||||
| 144 | \UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload', |
||||||
| 145 | ]; |
||||||
| 146 | |||||||
| 147 | public static function streamNotAvailable() |
||||||
| 148 | { |
||||||
| 149 | return new RuntimeException('Stream is not available, because the file was previously moved'); |
||||||
| 150 | } |
||||||
| 151 | |||||||
| 152 | public static function targetPathIsInvalid() |
||||||
| 153 | { |
||||||
| 154 | return new InvalidArgumentException('The provided path for moveTo operation is not valid'); |
||||||
| 155 | } |
||||||
| 156 | |||||||
| 157 | public static function fileAlreadyMoved() |
||||||
| 158 | { |
||||||
| 159 | return new RuntimeException('File is not available, because it was previously moved'); |
||||||
| 160 | } |
||||||
| 161 | |||||||
| 162 | public static function fileNotSupported() |
||||||
| 163 | { |
||||||
| 164 | return new InvalidArgumentException('The uploaded file is not supported'); |
||||||
| 165 | } |
||||||
| 166 | |||||||
| 167 | public static function filenameCannotBeEmpty() |
||||||
| 168 | { |
||||||
| 169 | return new InvalidArgumentException('Filename cannot be empty'); |
||||||
| 170 | } |
||||||
| 171 | } |
||||||
| 172 |