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: * Creates a new HttpServerRequestUpload object
39: *
40: * @param StreamInterface|resource|string $resource
41: */
42: public function __construct(
43: $resource,
44: ?int $size = null,
45: int $error = \UPLOAD_ERR_OK,
46: ?string $clientFilename = null,
47: ?string $clientMediaType = null
48: ) {
49: $this->Size = $size;
50: $this->Error = $error;
51: $this->ClientFilename = $clientFilename;
52: $this->ClientMediaType = $clientMediaType;
53:
54: if ($this->Error !== \UPLOAD_ERR_OK) {
55: return;
56: }
57:
58: if ($resource instanceof StreamInterface) {
59: $this->Stream = $resource;
60: } elseif (File::isStream($resource)) {
61: $this->Stream = new HttpStream($resource);
62: } elseif (is_string($resource)) {
63: $this->File = $resource;
64: } else {
65: throw new InvalidArgumentTypeException(
66: 1,
67: 'resource',
68: 'StreamInterface|resource|string',
69: $resource
70: );
71: }
72: }
73:
74: /**
75: * @inheritDoc
76: */
77: public function getStream(): StreamInterface
78: {
79: $this->assertIsValid();
80:
81: return $this->Stream ?? new HttpStream(File::open($this->File, 'r'));
82: }
83:
84: /**
85: * @inheritDoc
86: */
87: public function moveTo(string $targetPath): void
88: {
89: $this->assertIsValid();
90:
91: if (isset($this->File)) {
92: if (\PHP_SAPI === 'cli') {
93: $result = @rename($this->File, $targetPath);
94: } else {
95: $result = @move_uploaded_file($this->File, $targetPath);
96: }
97: if ($result === false) {
98: $error = error_get_last();
99: throw new UploadedFileException($error['message'] ?? sprintf(
100: 'Error moving uploaded file %s to %s',
101: $this->File,
102: $targetPath,
103: ));
104: }
105: } else {
106: $target = new HttpStream(File::open($targetPath, 'w'));
107: HttpStream::copyToStream($this->Stream, $target);
108: }
109:
110: $this->IsMoved = true;
111: }
112:
113: /**
114: * @inheritDoc
115: */
116: public function getSize(): ?int
117: {
118: return $this->Size;
119: }
120:
121: /**
122: * @inheritDoc
123: */
124: public function getError(): int
125: {
126: return $this->Error;
127: }
128:
129: /**
130: * @inheritDoc
131: */
132: public function getClientFilename(): ?string
133: {
134: return $this->ClientFilename;
135: }
136:
137: /**
138: * @inheritDoc
139: */
140: public function getClientMediaType(): ?string
141: {
142: return $this->ClientMediaType;
143: }
144:
145: private function assertIsValid(): void
146: {
147: if ($this->Error !== \UPLOAD_ERR_OK) {
148: throw new UploadedFileException(sprintf(
149: 'Upload failed (%d: %s)',
150: $this->Error,
151: static::ERROR_MESSAGE[$this->Error] ?? '',
152: ));
153: }
154:
155: if ($this->IsMoved) {
156: throw new UploadedFileException('Uploaded file already moved');
157: }
158: }
159: }
160: