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