]> git.mxchange.org Git - friendica.git/blob - src/Util/HTTPInputData.php
Catch exceptions for Worker::AddContact()
[friendica.git] / src / Util / HTTPInputData.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2022, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Util;
23
24 /**
25  * Derived from the work of Reid Johnson <https://codereview.stackexchange.com/users/4020/reid-johnson>
26  * @see https://codereview.stackexchange.com/questions/69882/parsing-multipart-form-data-in-php-for-put-requests
27  */
28 class HTTPInputData
29 {
30         /** @var array The $_SERVER variable */
31         protected $server;
32
33         public function __construct(array $server)
34         {
35                 $this->server = $server;
36         }
37
38         /**
39          * Process the PHP input stream and creates an array with its content
40          *
41          * @return array|array[]
42          */
43         public function process(): array
44         {
45                 $content_parts = explode(';', $this->server['CONTENT_TYPE'] ?? 'application/x-www-form-urlencoded');
46
47                 $boundary = '';
48                 $encoding = '';
49
50                 $content_type = array_shift($content_parts);
51
52                 foreach ($content_parts as $part) {
53                         if (strpos($part, 'boundary') !== false) {
54                                 $part = explode('=', $part, 2);
55                                 if (!empty($part[1])) {
56                                         $boundary = '--' . $part[1];
57                                 }
58                         } elseif (strpos($part, 'charset') !== false) {
59                                 $part = explode('=', $part, 2);
60                                 if (!empty($part[1])) {
61                                         $encoding = $part[1];
62                                 }
63                         }
64                         if ($boundary !== '' && $encoding !== '') {
65                                 break;
66                         }
67                 }
68
69                 if ($content_type == 'multipart/form-data') {
70                         return $this->fetchFromMultipart($boundary);
71                 }
72
73                 // can be handled by built in PHP functionality
74                 $content = static::getPhpInputContent();
75
76                 $variables = json_decode($content, true);
77
78                 if (empty($variables)) {
79                         parse_str($content, $variables);
80                 }
81
82                 return ['variables' => $variables, 'files' => []];
83         }
84
85         private function fetchFromMultipart(string $boundary): array
86         {
87                 $result = ['variables' => [], 'files' => []];
88
89                 $stream = static::getPhpInputStream();
90
91                 $sanity = fgets($stream, strlen($boundary) + 5);
92
93                 // malformed file, boundary should be first item
94                 if (rtrim($sanity) !== $boundary) {
95                         return $result;
96                 }
97
98                 $raw_headers = '';
99
100                 while (($chunk = fgets($stream)) !== false) {
101                         if ($chunk === $boundary) {
102                                 continue;
103                         }
104
105                         if (!empty(trim($chunk))) {
106                                 $raw_headers .= $chunk;
107                                 continue;
108                         }
109
110                         $result = $this->parseRawHeader($stream, $raw_headers, $boundary, $result);
111
112                         $raw_headers = '';
113                 }
114
115                 fclose($stream);
116
117                 return $result;
118         }
119
120         private function parseRawHeader($stream, string $raw_headers, string $boundary, array $result)
121         {
122                 $variables = $result['variables'];
123                 $files     = $result['files'];
124
125                 $headers = [];
126
127                 foreach (explode("\r\n", $raw_headers) as $header) {
128                         if (strpos($header, ':') === false) {
129                                 continue;
130                         }
131                         [$name, $value] = explode(':', $header, 2);
132
133                         $headers[strtolower($name)] = ltrim($value, ' ');
134                 }
135
136                 if (!isset($headers['content-disposition'])) {
137                         return ['variables' => $variables, 'files' => $files];
138                 }
139
140                 if (!preg_match('/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/', $headers['content-disposition'], $matches)) {
141                         return ['variables' => $variables, 'files' => $files];
142                 }
143
144                 $name     = $matches[2];
145                 $filename = $matches[4] ?? '';
146
147                 if (!empty($filename)) {
148                         $files[$name] = static::fetchFileData($stream, $boundary, $headers, $filename);
149                         return ['variables' => $variables, 'files' => $files];
150                 } else {
151                         $variables = $this->fetchVariables($stream, $boundary, $headers, $name, $variables);
152                 }
153
154                 return ['variables' => $variables, 'files' => $files];
155         }
156
157         protected function fetchFileData($stream, string $boundary, array $headers, string $filename)
158         {
159                 $error = UPLOAD_ERR_OK;
160
161                 if (isset($headers['content-type'])) {
162                         $tmp = explode(';', $headers['content-type']);
163
164                         $contentType = $tmp[0];
165                 } else {
166                         $contentType = 'unknown';
167                 }
168
169                 $tmpnam     = tempnam(ini_get('upload_tmp_dir'), 'php');
170                 $fileHandle = fopen($tmpnam, 'wb');
171
172                 if ($fileHandle === false) {
173                         $error = UPLOAD_ERR_CANT_WRITE;
174                 } else {
175                         $lastLine = null;
176                         while (($chunk = fgets($stream, 8096)) !== false && strpos($chunk, $boundary) !== 0) {
177                                 if ($lastLine !== null) {
178                                         if (!fwrite($fileHandle, $lastLine)) {
179                                                 $error = UPLOAD_ERR_CANT_WRITE;
180                                                 break;
181                                         }
182                                 }
183                                 $lastLine = $chunk;
184                         }
185
186                         if ($lastLine !== null && $error !== UPLOAD_ERR_CANT_WRITE) {
187                                 if (!fwrite($fileHandle, rtrim($lastLine, "\r\n"))) {
188                                         $error = UPLOAD_ERR_CANT_WRITE;
189                                 }
190                         }
191                 }
192
193                 return [
194                         'name'     => $filename,
195                         'type'     => $contentType,
196                         'tmp_name' => $tmpnam,
197                         'error'    => $error,
198                         'size'     => filesize($tmpnam)
199                 ];
200         }
201
202         private function fetchVariables($stream, string $boundary, array $headers, string $name, array $variables)
203         {
204                 $fullValue = '';
205                 $lastLine  = null;
206
207                 while (($chunk = fgets($stream)) !== false && strpos($chunk, $boundary) !== 0) {
208                         if ($lastLine !== null) {
209                                 $fullValue .= $lastLine;
210                         }
211
212                         $lastLine = $chunk;
213                 }
214
215                 if ($lastLine !== null) {
216                         $fullValue .= rtrim($lastLine, "\r\n");
217                 }
218
219                 if (isset($headers['content-type'])) {
220                         $encoding = '';
221
222                         foreach (explode(';', $headers['content-type']) as $part) {
223                                 if (strpos($part, 'charset') !== false) {
224                                         $part = explode($part, '=', 2);
225                                         if (isset($part[1])) {
226                                                 $encoding = $part[1];
227                                         }
228                                         break;
229                                 }
230                         }
231
232                         if ($encoding !== '' && strtoupper($encoding) !== 'UTF-8' && strtoupper($encoding) !== 'UTF8') {
233                                 $tmp = mb_convert_encoding($fullValue, 'UTF-8', $encoding);
234                                 if ($tmp !== false) {
235                                         $fullValue = $tmp;
236                                 }
237                         }
238                 }
239
240                 $fullValue = $name . '=' . $fullValue;
241
242                 $tmp = [];
243                 parse_str($fullValue, $tmp);
244
245                 return $this->expandVariables(explode('[', $name), $variables, $tmp);
246         }
247
248         private function expandVariables(array $names, $variables, array $values)
249         {
250                 if (!is_array($variables)) {
251                         return $values;
252                 }
253
254                 $name = rtrim(array_shift($names), ']');
255                 if ($name !== '') {
256                         $name = $name . '=p';
257
258                         $tmp = [];
259                         parse_str($name, $tmp);
260
261                         $tmp  = array_keys($tmp);
262                         $name = reset($tmp);
263                 }
264
265                 if ($name === '') {
266                         $variables[] = reset($values);
267                 } elseif (isset($variables[$name]) && isset($values[$name])) {
268                         $variables[$name] = $this->expandVariables($names, $variables[$name], $values[$name]);
269                 } elseif (isset($values[$name])) {
270                         $variables[$name] = $values[$name];
271                 }
272
273                 return $variables;
274         }
275
276         /**
277          * Returns the current PHP input stream
278          * Mainly used for test doubling
279          *
280          * @return false|resource
281          */
282         protected function getPhpInputStream()
283         {
284                 return fopen('php://input', 'rb');
285         }
286
287         /**
288          * Returns the content of the current PHP input
289          * Mainly used for test doubling
290          *
291          * @return false|string
292          */
293         protected function getPhpInputContent()
294         {
295                 return file_get_contents('php://input');
296         }
297 }