1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\Http; |
4: | |
5: | use Psr\Http\Message\StreamInterface; |
6: | use Salient\Contract\Http\HttpMultipartStreamPartInterface; |
7: | use Salient\Contract\Http\MimeType; |
8: | use Salient\Core\Concern\ImmutableTrait; |
9: | use Salient\Utility\Exception\InvalidArgumentTypeException; |
10: | use Salient\Utility\Exception\InvalidRuntimeConfigurationException; |
11: | use Salient\Utility\File; |
12: | use Salient\Utility\Regex; |
13: | use Salient\Utility\Str; |
14: | use InvalidArgumentException; |
15: | use LogicException; |
16: | |
17: | |
18: | |
19: | |
20: | |
21: | |
22: | class HttpMultipartStreamPart implements HttpMultipartStreamPartInterface |
23: | { |
24: | use ImmutableTrait; |
25: | |
26: | protected ?string $Name; |
27: | protected ?string $Filename; |
28: | protected ?string $FallbackFilename; |
29: | protected ?string $MediaType; |
30: | protected StreamInterface $Content; |
31: | |
32: | |
33: | |
34: | |
35: | public function __construct( |
36: | $content, |
37: | ?string $name = null, |
38: | ?string $filename = null, |
39: | ?string $mediaType = null, |
40: | ?string $fallbackFilename = null |
41: | ) { |
42: | $this->Name = $name; |
43: | $this->Filename = Str::coalesce($filename, null); |
44: | $this->FallbackFilename = $this->filterFallbackFilename( |
45: | Str::coalesce($fallbackFilename, null), |
46: | $this->Filename |
47: | ); |
48: | $this->MediaType = Str::coalesce($mediaType, null); |
49: | $this->Content = $this->filterContent($content); |
50: | } |
51: | |
52: | |
53: | |
54: | |
55: | |
56: | |
57: | |
58: | |
59: | public static function fromFile( |
60: | string $filename, |
61: | ?string $name = null, |
62: | ?string $uploadFilename = null, |
63: | ?string $mediaType = null, |
64: | ?string $fallbackFilename = null |
65: | ): self { |
66: | if (!is_file($filename)) { |
67: | throw new InvalidArgumentException(sprintf( |
68: | 'File not found: %s', |
69: | $filename, |
70: | )); |
71: | } |
72: | |
73: | return new self( |
74: | File::open($filename, 'r'), |
75: | $name, |
76: | $uploadFilename ?? basename($filename), |
77: | self::getFileMediaType($filename, $mediaType), |
78: | $fallbackFilename, |
79: | ); |
80: | } |
81: | |
82: | |
83: | |
84: | |
85: | protected static function getFileMediaType(string $filename, ?string $mediaType = null): string |
86: | { |
87: | if ($mediaType === null) { |
88: | if (!extension_loaded('fileinfo')) { |
89: | |
90: | throw new InvalidRuntimeConfigurationException( |
91: | "'fileinfo' extension required for MIME type detection" |
92: | ); |
93: | |
94: | } |
95: | $mediaType = @mime_content_type($filename); |
96: | if ($mediaType === false) { |
97: | |
98: | $mediaType = MimeType::BINARY; |
99: | |
100: | } |
101: | } |
102: | |
103: | return $mediaType; |
104: | } |
105: | |
106: | |
107: | |
108: | |
109: | public function getName(): string |
110: | { |
111: | if ($this->Name === null) { |
112: | throw new LogicException('Name is not set'); |
113: | } |
114: | return $this->Name; |
115: | } |
116: | |
117: | |
118: | |
119: | |
120: | public function getFilename(): ?string |
121: | { |
122: | return $this->Filename; |
123: | } |
124: | |
125: | |
126: | |
127: | |
128: | public function getFallbackFilename(): ?string |
129: | { |
130: | return $this->FallbackFilename; |
131: | } |
132: | |
133: | |
134: | |
135: | |
136: | public function getMediaType(): ?string |
137: | { |
138: | return $this->MediaType; |
139: | } |
140: | |
141: | |
142: | |
143: | |
144: | public function getContent(): StreamInterface |
145: | { |
146: | return $this->Content; |
147: | } |
148: | |
149: | |
150: | |
151: | |
152: | public function withName(string $name): HttpMultipartStreamPartInterface |
153: | { |
154: | return $this->with('Name', $name); |
155: | } |
156: | |
157: | |
158: | |
159: | |
160: | |
161: | |
162: | |
163: | |
164: | protected function filterFallbackFilename(?string $fallbackFilename, ?string $filename): ?string |
165: | { |
166: | $filename = $fallbackFilename ?? $filename; |
167: | if ($filename === null) { |
168: | return null; |
169: | } |
170: | if ( |
171: | !Str::isAscii($filename) |
172: | || Regex::match('/%[0-9a-f]{2}|\\\\|"/i', $filename) |
173: | ) { |
174: | if ($fallbackFilename === null) { |
175: | return null; |
176: | } |
177: | throw new InvalidArgumentException( |
178: | sprintf('Invalid fallback filename: %s', $filename) |
179: | ); |
180: | } |
181: | return $filename; |
182: | } |
183: | |
184: | |
185: | |
186: | |
187: | protected function filterContent($content): StreamInterface |
188: | { |
189: | if ($content instanceof StreamInterface) { |
190: | return $content; |
191: | } |
192: | if (is_string($content) || $content === null) { |
193: | return HttpStream::fromString((string) $content); |
194: | } |
195: | try { |
196: | return new HttpStream($content); |
197: | } catch (InvalidArgumentException $ex) { |
198: | throw new InvalidArgumentTypeException( |
199: | 1, |
200: | 'content', |
201: | 'StreamInterface|resource|string|null', |
202: | $content |
203: | ); |
204: | } |
205: | } |
206: | } |
207: | |