Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
141 / 141
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Message
100.00% covered (success)
100.00%
141 / 141
100.00% covered (success)
100.00%
6 / 6
61
100.00% covered (success)
100.00%
1 / 1
 parseMessage
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
7
 parseForm
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
10
 createForm
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
16
 parseHeaders
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
 parseBody
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 parsePart
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
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 */
14namespace Pop\Mime;
15
16use Pop\Mime\Part\Header;
17use 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 */
29class 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}