1: <?php declare(strict_types=1);
2:
3: namespace Salient\Http;
4:
5: use Psr\Http\Message\StreamInterface;
6: use Psr\Http\Message\UploadedFileInterface;
7: use Salient\Http\Exception\UploadedFileException;
8: use Salient\Utility\Exception\InvalidArgumentTypeException;
9: use Salient\Utility\File;
10:
11: /**
12: * A PSR-7 uploaded file (incoming, server-side)
13: *
14: * @api
15: */
16: class HttpServerRequestUpload implements UploadedFileInterface
17: {
18: protected const ERROR_MESSAGE = [
19: \UPLOAD_ERR_OK => 'There is no error, the file uploaded with success',
20: \UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
21: \UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
22: \UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded',
23: \UPLOAD_ERR_NO_FILE => 'No file was uploaded',
24: \UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder',
25: \UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
26: \UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload',
27: ];
28:
29: protected StreamInterface $Stream;
30: protected string $File;
31: protected ?int $Size;
32: protected int $Error;
33: protected ?string $ClientFilename;
34: protected ?string $ClientMediaType;
35: private bool $IsMoved = false;
36:
37: /**
38: * @param StreamInterface|resource|string $resource
39: */
40: public function __construct(
41: $resource,
42: ?int $size = null,
43: int $error = \UPLOAD_ERR_OK,
44: ?string $clientFilename = null,
45: ?string $clientMediaType = null
46: ) {
47: $this->Size = $size;
48: $this->Error = $error;
49: $this->ClientFilename = $clientFilename;
50: $this->ClientMediaType = $clientMediaType;
51:
52: if ($this->Error !== \UPLOAD_ERR_OK) {
53: return;
54: }
55:
56: if ($resource instanceof StreamInterface) {
57: $this->Stream = $resource;
58: } elseif (File::isStream($resource)) {
59: $this->Stream = new HttpStream($resource);
60: } elseif (is_string($resource)) {
61: $this->File = $resource;
62: } else {
63: throw new InvalidArgumentTypeException(
64: 1,
65: 'resource',
66: 'StreamInterface|resource|string',
67: $resource
68: );
69: }
70: }
71:
72: /**
73: * @inheritDoc
74: */
75: public function getStream(): StreamInterface
76: {
77: $this->assertIsValid();
78:
79: return $this->Stream ?? new HttpStream(File::open($this->File, 'r'));
80: }
81:
82: /**
83: * @inheritDoc
84: */
85: public function moveTo(string $targetPath): void
86: {
87: $this->assertIsValid();
88:
89: if (isset($this->File)) {
90: if (\PHP_SAPI === 'cli') {
91: $result = @rename($this->File, $targetPath);
92: } else {
93: $result = @move_uploaded_file($this->File, $targetPath);
94: }
95: if ($result === false) {
96: $error = error_get_last();
97: throw new UploadedFileException($error['message'] ?? sprintf(
98: 'Error moving uploaded file %s to %s',
99: $this->File,
100: $targetPath,
101: ));
102: }
103: } else {
104: $target = new HttpStream(File::open($targetPath, 'w'));
105: HttpStream::copyToStream($this->Stream, $target);
106: }
107:
108: $this->IsMoved = true;
109: }
110:
111: /**
112: * @inheritDoc
113: */
114: public function getSize(): ?int
115: {
116: return $this->Size;
117: }
118:
119: /**
120: * @inheritDoc
121: */
122: public function getError(): int
123: {
124: return $this->Error;
125: }
126:
127: /**
128: * @inheritDoc
129: */
130: public function getClientFilename(): ?string
131: {
132: return $this->ClientFilename;
133: }
134:
135: /**
136: * @inheritDoc
137: */
138: public function getClientMediaType(): ?string
139: {
140: return $this->ClientMediaType;
141: }
142:
143: private function assertIsValid(): void
144: {
145: if ($this->Error !== \UPLOAD_ERR_OK) {
146: throw new UploadedFileException(sprintf(
147: 'Upload failed (%d: %s)',
148: $this->Error,
149: static::ERROR_MESSAGE[$this->Error] ?? '',
150: ));
151: }
152:
153: if ($this->IsMoved) {
154: throw new UploadedFileException('Uploaded file already moved');
155: }
156: }
157: }
158: