1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\Http\Message; |
4: | |
5: | use Psr\Http\Message\StreamInterface as PsrStreamInterface; |
6: | use Salient\Contract\Http\Message\StreamPartInterface; |
7: | use Salient\Core\Concern\ImmutableTrait; |
8: | use Salient\Utility\File; |
9: | use Salient\Utility\Regex; |
10: | use Salient\Utility\Str; |
11: | use InvalidArgumentException; |
12: | use LogicException; |
13: | |
14: | |
15: | |
16: | |
17: | class StreamPart implements StreamPartInterface |
18: | { |
19: | use HasBody; |
20: | use ImmutableTrait; |
21: | |
22: | private ?string $Name; |
23: | private ?string $Filename; |
24: | private ?string $AsciiFilename; |
25: | private ?string $MediaType; |
26: | private PsrStreamInterface $Body; |
27: | |
28: | |
29: | |
30: | |
31: | |
32: | |
33: | public function __construct( |
34: | $body, |
35: | ?string $name = null, |
36: | ?string $filename = null, |
37: | ?string $mediaType = null, |
38: | ?string $asciiFilename = null |
39: | ) { |
40: | $this->Name = $name; |
41: | $this->Filename = Str::coalesce($filename, null); |
42: | $this->AsciiFilename = $this->filterAsciiFilename( |
43: | Str::coalesce($asciiFilename, null), |
44: | $this->Filename, |
45: | ); |
46: | $this->MediaType = Str::coalesce($mediaType, null); |
47: | $this->Body = $this->filterBody($body); |
48: | } |
49: | |
50: | |
51: | |
52: | |
53: | |
54: | |
55: | |
56: | |
57: | public static function fromFile( |
58: | string $filename, |
59: | ?string $name = null, |
60: | ?string $uploadFilename = null, |
61: | ?string $mediaType = null, |
62: | ?string $asciiFilename = null |
63: | ): self { |
64: | return new self( |
65: | File::open($filename, 'r'), |
66: | $name, |
67: | $uploadFilename ?? basename($filename), |
68: | self::filterFileMediaType($mediaType, $filename), |
69: | $asciiFilename, |
70: | ); |
71: | } |
72: | |
73: | |
74: | |
75: | |
76: | protected static function filterFileMediaType( |
77: | ?string $mediaType, |
78: | string $filename |
79: | ): string { |
80: | if ($mediaType !== null) { |
81: | return $mediaType; |
82: | } |
83: | |
84: | $mediaType = extension_loaded('fileinfo') |
85: | ? @mime_content_type($filename) |
86: | : false; |
87: | return $mediaType === false |
88: | ? self::TYPE_BINARY |
89: | : $mediaType; |
90: | } |
91: | |
92: | |
93: | |
94: | |
95: | public function getName(): string |
96: | { |
97: | if ($this->Name === null) { |
98: | throw new LogicException('Name not applied'); |
99: | } |
100: | return $this->Name; |
101: | } |
102: | |
103: | |
104: | |
105: | |
106: | public function getFilename(): ?string |
107: | { |
108: | return $this->Filename; |
109: | } |
110: | |
111: | |
112: | |
113: | |
114: | public function getAsciiFilename(): ?string |
115: | { |
116: | return $this->AsciiFilename; |
117: | } |
118: | |
119: | |
120: | |
121: | |
122: | public function getMediaType(): ?string |
123: | { |
124: | return $this->MediaType; |
125: | } |
126: | |
127: | |
128: | |
129: | |
130: | public function getBody(): PsrStreamInterface |
131: | { |
132: | return $this->Body; |
133: | } |
134: | |
135: | |
136: | |
137: | |
138: | public function withName(string $name): StreamPartInterface |
139: | { |
140: | return $this->with('Name', $name); |
141: | } |
142: | |
143: | private function filterAsciiFilename(?string $asciiFilename, ?string $filename): ?string |
144: | { |
145: | $filename = $asciiFilename ?? $filename; |
146: | if ($filename === null) { |
147: | return null; |
148: | } |
149: | |
150: | |
151: | if ( |
152: | !Str::isAscii($filename) |
153: | || Regex::match('/%[0-9a-f]{2}|\\\\|"/i', $filename) |
154: | ) { |
155: | if ($asciiFilename !== null) { |
156: | throw new InvalidArgumentException( |
157: | sprintf('Invalid ASCII filename: %s', $filename), |
158: | ); |
159: | } |
160: | return null; |
161: | } |
162: | return $filename; |
163: | } |
164: | } |
165: | |