]> git.mxchange.org Git - friendica.git/blob - src/Protocol/HTTP/MediaType.php
C2S: Improve C2S-API, fix inbox endpoint
[friendica.git] / src / Protocol / HTTP / MediaType.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2023, 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\Protocol\HTTP;
23
24 /**
25  * @see https://httpwg.org/specs/rfc9110.html#media.type
26  *
27  * @property-read string $type
28  * @property-read string $subType
29  * @property-read string $parameters
30  */
31 final class MediaType
32 {
33         const DQUOTE = '"';
34         const DIGIT  = '0-9';
35         const ALPHA  = 'a-zA-Z';
36
37         // @see https://www.charset.org/charsets/us-ascii
38         const VCHAR = "\\x21-\\x7E";
39
40         const SYMBOL_NO_DELIM = "!#$%&'*+-.^_`|~";
41
42         const OBSTEXT = "\\x80-\\xFF";
43
44         const QDTEXT = "\t \\x21\\x23-\\x5B\\x5D-\\x7E" . self::OBSTEXT;
45
46         /**
47          * @var string
48          */
49         private $type;
50
51         /**
52          * @var @string
53          */
54         private $subType;
55
56         /**
57          * @var string[]
58          */
59         private $parameters;
60
61         public function __construct(string $type, string $subType, array $parameters = [])
62         {
63                 if (!self::isToken($type)) {
64                         throw new \InvalidArgumentException("Type isn't a valid token: " . $type);
65                 }
66
67                 if (!self::isToken($subType)) {
68                         throw new \InvalidArgumentException("Subtype isn't a valid token: " . $subType);
69                 }
70
71                 foreach ($parameters as $key => $value) {
72                         if (!self::isToken($key)) {
73                                 throw new \InvalidArgumentException("Parameter key isn't a valid token: " . $key);
74                         }
75
76                         if (!self::isToken($value) && !self::isQuotableString($value)) {
77                                 throw new \InvalidArgumentException("Parameter value isn't a valid token or a quotable string: " . $value);
78                         }
79                 }
80
81                 $this->type       = $type;
82                 $this->subType    = $subType;
83                 $this->parameters = $parameters;
84         }
85
86         public function __get(string $name)
87         {
88                 if (!isset($this->$name)) {
89                         throw new \InvalidArgumentException('Unknown property ' . $name);
90                 }
91
92                 return $this->$name;
93         }
94
95         public static function fromContentType(string $contentType): self
96         {
97                 if (!$contentType) {
98                         throw new \InvalidArgumentException('Provided string is empty');
99                 }
100
101                 $parts         = explode(';', $contentType);
102                 $mimeTypeParts = explode('/', trim(array_shift($parts)));
103                 if (count($mimeTypeParts) !== 2) {
104                         throw new \InvalidArgumentException('Provided string doesn\'t look like a MIME type: ' . $contentType);
105                 }
106
107                 list($type, $subType) = $mimeTypeParts;
108
109                 $parameters = [];
110                 foreach ($parts as $parameterString) {
111                         if (!trim($parameterString)) {
112                                 continue;
113                         }
114
115                         $parameterParts = explode('=', trim($parameterString));
116
117                         if (count($parameterParts) < 2) {
118                                 throw new \InvalidArgumentException('Parameter lacks a value: ' . $parameterString);
119                         }
120
121                         if (count($parameterParts) > 2) {
122                                 throw new \InvalidArgumentException('Parameter has too many values: ' . $parameterString);
123                         }
124
125                         list($key, $value) = $parameterParts;
126
127                         if (!self::isToken($value) && !self::isQuotedString($value)) {
128                                 throw new \InvalidArgumentException("Parameter value isn't a valid token or a quoted string: \"" . $value . '"');
129                         }
130
131                         if (self::isQuotedString($value)) {
132                                 $value = self::extractQuotedStringValue($value);
133                         }
134
135                         // Parameter keys are case-insensitive, values are not
136                         $parameters[strtolower($key)] = $value;
137                 }
138
139                 return new self($type, $subType, $parameters);
140         }
141
142         public function __toString(): string
143         {
144                 $parameters = $this->parameters;
145
146                 array_walk($parameters, function (&$value, $key) {
147                         $value = '; ' . $key . '=' . (self::isToken($value) ? $value : '"' . addcslashes($value, '"\\') . '"');
148                 });
149
150                 return $this->type . '/' . $this->subType . implode($parameters);
151         }
152
153         /**
154          * token          = 1*tchar
155          * tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*"
156          *                / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
157          *                / DIGIT / ALPHA
158          *                ; any VCHAR, except delimiters
159          *
160          * @see https://httpwg.org/specs/rfc9110.html#tokens
161          *
162          * @param string $string
163          * @return false|int
164          */
165         private static function isToken(string $string)
166         {
167                 $symbol = preg_quote(self::SYMBOL_NO_DELIM, '/');
168                 $digit  = self::DIGIT;
169                 $alpha  = self::ALPHA;
170
171                 $pattern = "/^[$symbol$digit$alpha]+$/";
172
173                 return preg_match($pattern, $string);
174         }
175
176         /**
177          * quoted-string  = DQUOTE *( qdtext / quoted-pair ) DQUOTE
178          * qdtext         = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
179          *
180          * @see https://httpwg.org/specs/rfc9110.html#quoted.strings
181          *
182          * @param string $string
183          * @return bool
184          */
185         private static function isQuotedString(string $string): bool
186         {
187                 $dquote = self::DQUOTE;
188
189                 $vchar = self::VCHAR;
190
191                 $obsText = self::OBSTEXT;
192
193                 $qdtext = '[' . self::QDTEXT . ']';
194
195                 $quotedPair = "\\\\[\t $vchar$obsText]";
196
197                 $pattern = "/^$dquote(?:$qdtext|$quotedPair)*$dquote$/";
198
199                 return preg_match($pattern, $string);
200         }
201
202         /**
203          * Is the string an extracted quoted string value?
204          *
205          * @param string $string
206          * @return bool
207          */
208         private static function isQuotableString(string $string): bool
209         {
210                 $vchar = self::VCHAR;
211
212                 $obsText = self::OBSTEXT;
213
214                 $qdtext = '[' . self::QDTEXT . ']';
215
216                 $quotedSingle = "[\t $vchar$obsText]";
217
218                 $pattern = "/^(?:$qdtext|$quotedSingle)*$/";
219
220                 return preg_match($pattern, $string);
221         }
222
223         /**
224          * Extracts the value from a quoted-string, removing quoted pairs
225          *
226          * @param string $value
227          * @return string
228          */
229         private static function extractQuotedStringValue(string $value): string
230         {
231                 return preg_replace_callback('/^"(.*)"$/', function ($matches) {
232                         $vchar   = self::VCHAR;
233                         $obsText = self::OBSTEXT;
234                         return preg_replace("/\\\\([\t $vchar$obsText])/", '$1', $matches[1]);
235                 }, $value);
236         }
237 }