Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
141 / 141 |
|
100.00% |
6 / 6 |
CRAP | |
100.00% |
1 / 1 |
Message | |
100.00% |
141 / 141 |
|
100.00% |
6 / 6 |
61 | |
100.00% |
1 / 1 |
parseMessage | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
7 | |||
parseForm | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
10 | |||
createForm | |
100.00% |
36 / 36 |
|
100.00% |
1 / 1 |
16 | |||
parseHeaders | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
5 | |||
parseBody | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
parsePart | |
100.00% |
42 / 42 |
|
100.00% |
1 / 1 |
18 |
1 | <?php |
2 | /** |
3 | * Pop PHP Framework (http://www.popphp.org/) |
4 | * |
5 | * @link https://github.com/popphp/popphp-framework |
6 | * @author Nick Sagona, III <dev@nolainteractive.com> |
7 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
8 | * @license http://www.popphp.org/license New BSD License |
9 | */ |
10 | |
11 | /** |
12 | * @namespace |
13 | */ |
14 | namespace Pop\Mime; |
15 | |
16 | use Pop\Mime\Part\Header; |
17 | use Pop\Mime\Part\Body; |
18 | |
19 | /** |
20 | * MIME message class |
21 | * |
22 | * @category Pop |
23 | * @package Pop\Mime |
24 | * @author Nick Sagona, III <dev@nolainteractive.com> |
25 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
26 | * @license http://www.popphp.org/license New BSD License |
27 | * @version 2.0.0 |
28 | */ |
29 | class Message extends Part |
30 | { |
31 | |
32 | /** |
33 | * Parse message |
34 | * |
35 | * @param string $messageString |
36 | * @return Message |
37 | */ |
38 | public static function parseMessage(string $messageString): Message |
39 | { |
40 | $headerString = substr($messageString, 0, strpos($messageString, "\r\n\r\n")); |
41 | $bodyString = substr($messageString, (strpos($messageString, "\r\n\r\n") + 4)); |
42 | |
43 | $headers = self::parseHeaders($headerString); |
44 | $boundary = null; |
45 | $parts = []; |
46 | |
47 | foreach ($headers as $header) { |
48 | foreach ($header->getValues() as $headerValue) { |
49 | if ($headerValue->hasParameter('boundary')) { |
50 | $boundary = $headerValue->getParameter('boundary'); |
51 | break; |
52 | } |
53 | } |
54 | } |
55 | |
56 | $partStrings = self::parseBody($bodyString, $boundary); |
57 | |
58 | foreach ($partStrings as $partString) { |
59 | $parts[] = self::parsePart($partString); |
60 | } |
61 | |
62 | $message = new self(); |
63 | |
64 | if (!empty($headers)) { |
65 | $message->addHeaders($headers); |
66 | } |
67 | if (!empty($parts)) { |
68 | $message->addParts($parts); |
69 | } |
70 | |
71 | return $message; |
72 | } |
73 | |
74 | /** |
75 | * Parse form data |
76 | * |
77 | * @param string $formString |
78 | * @return array |
79 | */ |
80 | public static function parseForm(string $formString): array |
81 | { |
82 | $form = self::parseMessage($formString); |
83 | $formData = []; |
84 | |
85 | foreach ($form->getParts() as $part) { |
86 | if (($part->hasHeader('Content-Disposition')) && (count($part->getHeader('Content-Disposition')->getValues()) == 1)) { |
87 | $disposition = $part->getHeader('Content-Disposition'); |
88 | if (($disposition->hasValue('form-data')) && ($disposition->getValue(0)->hasParameter('name'))) { |
89 | $name = $disposition->getValue(0)->getParameter('name'); |
90 | $contents = $part->getContents(); |
91 | $filename = ($disposition->getValue(0)->hasParameter('filename')) ? $disposition->getValue(0)->getParameter('filename') : null; |
92 | |
93 | if (str_ends_with($name, '[]')) { |
94 | $name = substr($name, 0, -2); |
95 | if (!isset($formData[$name])) { |
96 | $formData[$name] = []; |
97 | } |
98 | $formData[$name][] = $contents; |
99 | } else { |
100 | if ($filename !== null) { |
101 | $formData[$name] = [ |
102 | 'filename' => $filename, |
103 | 'contents' => $contents |
104 | ]; |
105 | } else { |
106 | $formData[$name] = $contents; |
107 | } |
108 | } |
109 | } |
110 | } |
111 | } |
112 | |
113 | return $formData; |
114 | } |
115 | |
116 | /** |
117 | * Create multipart form object |
118 | * |
119 | * @param array $fields |
120 | * @return Message |
121 | */ |
122 | public static function createForm(array $fields = []): Message |
123 | { |
124 | $message = new self(); |
125 | $header = new Header('Content-Type', new Header\Value('multipart/form-data', null, ['boundary' => $message->generateBoundary()])); |
126 | $message->addHeader($header); |
127 | |
128 | if (!empty($fields)) { |
129 | foreach ($fields as $name => $value) { |
130 | if (is_array($value)) { |
131 | // Is file |
132 | if (isset($value['filename'])) { |
133 | $parameters = ['name' => $name]; |
134 | $contentType = null; |
135 | $fileContents = null; |
136 | |
137 | foreach ($value as $key => $val) { |
138 | $key = strtolower($key); |
139 | if ($key == 'filename') { |
140 | $parameters['filename'] = basename($val); |
141 | } |
142 | if (($key == 'content-type') || ($key == 'contenttype') || |
143 | ($key == 'mime-type') || ($key == 'mimetype') || ($key == 'mime')) { |
144 | $contentType = $val; |
145 | } |
146 | } |
147 | |
148 | if (isset($value['contents'])) { |
149 | $fileContents = $value['contents']; |
150 | } else if (file_exists($value['filename'])) { |
151 | $fileContents = file_get_contents($value['filename']); |
152 | } |
153 | |
154 | $fieldPart = new Part(new Header('Content-Disposition', new Header\Value('form-data', null, $parameters))); |
155 | if ($contentType !== null) { |
156 | $fieldPart->addHeader('Content-Type', $contentType); |
157 | } |
158 | $fieldPart->setBody(new Body($fileContents)); |
159 | $message->addPart($fieldPart); |
160 | } else { |
161 | foreach ($value as $val) { |
162 | $fieldPart = new Part( |
163 | new Header('Content-Disposition', new Header\Value('form-data', null, ['name' => $name . '[]'])) |
164 | ); |
165 | $fieldPart->setBody(new Body($val, Body::RAW_URL)); |
166 | $message->addPart($fieldPart); |
167 | } |
168 | } |
169 | } else { |
170 | $fieldPart = new Part(new Header('Content-Disposition', new Header\Value('form-data', null, ['name' => $name]))); |
171 | $fieldPart->setBody(new Body($value, Body::RAW_URL)); |
172 | $message->addPart($fieldPart); |
173 | } |
174 | } |
175 | } |
176 | |
177 | return $message; |
178 | } |
179 | |
180 | /** |
181 | * Parse message header string |
182 | * |
183 | * @param string $headerString |
184 | * @return array |
185 | */ |
186 | public static function parseHeaders(string $headerString): array |
187 | { |
188 | $headers = []; |
189 | $matches = []; |
190 | preg_match_all('/[a-zA-Z-]+:/', $headerString, $matches, PREG_OFFSET_CAPTURE); |
191 | |
192 | if (isset($matches[0]) && (count($matches[0]) > 0)) { |
193 | $length = count($matches[0]); |
194 | for ($i = 0; $i < $length; $i++) { |
195 | if (isset($matches[0][$i + 1][1])) { |
196 | $start = $matches[0][$i][1] + strlen($matches[0][$i][0]); |
197 | $offset = $matches[0][$i + 1][1]; |
198 | $value = substr($headerString, 0, $offset); |
199 | $value = trim(substr($value, $start)); |
200 | } else { |
201 | $start = strpos($headerString, $matches[0][$i][0]) + strlen($matches[0][$i][0]); |
202 | $value = substr($headerString, $start); |
203 | } |
204 | $headers[] = Header::parse($matches[0][$i][0] . ' ' . trim($value)); |
205 | } |
206 | } |
207 | |
208 | return $headers; |
209 | } |
210 | |
211 | /** |
212 | * Parse message body string |
213 | * |
214 | * @param string $bodyString |
215 | * @param ?string $boundary |
216 | * @return array |
217 | */ |
218 | public static function parseBody(string $bodyString, ?string $boundary = null): array |
219 | { |
220 | if (str_contains($bodyString, '--' . $boundary)) { |
221 | $parts = explode('--' . $boundary, $bodyString); |
222 | if ((strpos($bodyString, '--' . $boundary) > 0) && isset($parts[0])) { |
223 | unset($parts[0]); |
224 | } |
225 | } else { |
226 | $parts = [$bodyString]; |
227 | } |
228 | |
229 | return array_values(array_filter(array_map('trim', $parts), function ($value) { |
230 | return (!empty($value) && ($value != '--')); |
231 | })); |
232 | } |
233 | |
234 | /** |
235 | * Parse message part string |
236 | * |
237 | * @param string $partString |
238 | * @return Part|array |
239 | */ |
240 | public static function parsePart(string $partString): Part|array |
241 | { |
242 | $headers = []; |
243 | |
244 | if (str_contains($partString, "\r\n\r\n")) { |
245 | $headerString = substr($partString, 0, strpos($partString, "\r\n\r\n")); |
246 | $bodyString = trim(substr($partString, (strpos($partString, "\r\n\r\n") + 4))); |
247 | $headers = self::parseHeaders($headerString); |
248 | } else { |
249 | $bodyString = trim($partString); |
250 | } |
251 | |
252 | $part = new Part(); |
253 | |
254 | if (!empty($headers)) { |
255 | $part->addHeaders($headers); |
256 | } |
257 | |
258 | $boundary = null; |
259 | foreach ($part->getHeaders() as $header) { |
260 | foreach ($header->getValues() as $headerValue) { |
261 | if ($headerValue->hasParameter('boundary')) { |
262 | $boundary = $headerValue->getParameter('boundary'); |
263 | break; |
264 | } |
265 | } |
266 | } |
267 | |
268 | if (!empty($bodyString)) { |
269 | if ($boundary !== null) { |
270 | $subPartStrings = self::parseBody($bodyString, $boundary); |
271 | $subParts = []; |
272 | |
273 | foreach ($subPartStrings as $subPartString) { |
274 | $subParts[] = self::parsePart($subPartString); |
275 | } |
276 | return $subParts; |
277 | } else { |
278 | $encoding = null; |
279 | $isFile = (($part->hasHeader('Content-Disposition')) && |
280 | ($part->getHeader('Content-Disposition')->isAttachment())); |
281 | $isForm = (($part->hasHeader('Content-Disposition')) && |
282 | ($part->getHeader('Content-Disposition')->hasValue('form-data'))); |
283 | if ($part->hasHeader('Content-Transfer-Encoding') && (count($part->getHeader('Content-Transfer-Encoding')->getValues()) == 1)) { |
284 | $encodingHeader = strtolower($part->getHeader('Content-Transfer-Encoding')->getValue(0)); |
285 | if ($encodingHeader == 'base64') { |
286 | $encoding = Body::BASE64; |
287 | } else if ($encodingHeader == 'quoted-printable') { |
288 | $encoding = Body::QUOTED; |
289 | } |
290 | } else if ($isForm) { |
291 | $encoding = Body::RAW_URL; |
292 | } |
293 | $body = new Body($bodyString, $encoding); |
294 | if ($encoding !== null) { |
295 | $body->setAsEncoded(true); |
296 | } |
297 | if ($isFile) { |
298 | $body->setAsFile(true); |
299 | } |
300 | $part->setBody($body); |
301 | } |
302 | } |
303 | |
304 | return $part; |
305 | } |
306 | |
307 | } |