3 * @copyright Copyright (C) 2010-2023, the Friendica project
5 * @license GNU AGPL version 3 or any later version
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.
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.
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/>.
22 namespace Friendica\Protocol\HTTP;
25 * @see https://httpwg.org/specs/rfc9110.html#media.type
27 * @property-read string $type
28 * @property-read string $subType
29 * @property-read string $parameters
35 const ALPHA = 'a-zA-Z';
37 // @see https://www.charset.org/charsets/us-ascii
38 const VCHAR = "\\x21-\\x7E";
40 const SYMBOL_NO_DELIM = "!#$%&'*+-.^_`|~";
42 const OBSTEXT = "\\x80-\\xFF";
44 const QDTEXT = "\t \\x21\\x23-\\x5B\\x5D-\\x7E" . self::OBSTEXT;
61 public function __construct(string $type, string $subType, array $parameters = [])
63 if (!self::isToken($type)) {
64 throw new \InvalidArgumentException("Type isn't a valid token: " . $type);
67 if (!self::isToken($subType)) {
68 throw new \InvalidArgumentException("Subtype isn't a valid token: " . $subType);
71 foreach ($parameters as $key => $value) {
72 if (!self::isToken($key)) {
73 throw new \InvalidArgumentException("Parameter key isn't a valid token: " . $key);
76 if (!self::isToken($value) && !self::isQuotableString($value)) {
77 throw new \InvalidArgumentException("Parameter value isn't a valid token or a quotable string: " . $value);
82 $this->subType = $subType;
83 $this->parameters = $parameters;
86 public function __get(string $name)
88 if (!isset($this->$name)) {
89 throw new \InvalidArgumentException('Unknown property ' . $name);
95 public static function fromContentType(string $contentType): self
98 throw new \InvalidArgumentException('Provided string is empty');
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);
107 list($type, $subType) = $mimeTypeParts;
110 foreach ($parts as $parameterString) {
111 if (!trim($parameterString)) {
115 $parameterParts = explode('=', trim($parameterString));
117 if (count($parameterParts) < 2) {
118 throw new \InvalidArgumentException('Parameter lacks a value: ' . $parameterString);
121 if (count($parameterParts) > 2) {
122 throw new \InvalidArgumentException('Parameter has too many values: ' . $parameterString);
125 list($key, $value) = $parameterParts;
127 if (!self::isToken($value) && !self::isQuotedString($value)) {
128 throw new \InvalidArgumentException("Parameter value isn't a valid token or a quoted string: \"" . $value . '"');
131 if (self::isQuotedString($value)) {
132 $value = self::extractQuotedStringValue($value);
135 // Parameter keys are case-insensitive, values are not
136 $parameters[strtolower($key)] = $value;
139 return new self($type, $subType, $parameters);
142 public function __toString(): string
144 $parameters = $this->parameters;
146 array_walk($parameters, function (&$value, $key) {
147 $value = '; ' . $key . '=' . (self::isToken($value) ? $value : '"' . addcslashes($value, '"\\') . '"');
150 return $this->type . '/' . $this->subType . implode($parameters);
155 * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
156 * / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
158 * ; any VCHAR, except delimiters
160 * @see https://httpwg.org/specs/rfc9110.html#tokens
162 * @param string $string
165 private static function isToken(string $string)
167 $symbol = preg_quote(self::SYMBOL_NO_DELIM, '/');
168 $digit = self::DIGIT;
169 $alpha = self::ALPHA;
171 $pattern = "/^[$symbol$digit$alpha]+$/";
173 return preg_match($pattern, $string);
177 * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
178 * qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
180 * @see https://httpwg.org/specs/rfc9110.html#quoted.strings
182 * @param string $string
185 private static function isQuotedString(string $string): bool
187 $dquote = self::DQUOTE;
189 $vchar = self::VCHAR;
191 $obsText = self::OBSTEXT;
193 $qdtext = '[' . self::QDTEXT . ']';
195 $quotedPair = "\\\\[\t $vchar$obsText]";
197 $pattern = "/^$dquote(?:$qdtext|$quotedPair)*$dquote$/";
199 return preg_match($pattern, $string);
203 * Is the string an extracted quoted string value?
205 * @param string $string
208 private static function isQuotableString(string $string): bool
210 $vchar = self::VCHAR;
212 $obsText = self::OBSTEXT;
214 $qdtext = '[' . self::QDTEXT . ']';
216 $quotedSingle = "[\t $vchar$obsText]";
218 $pattern = "/^(?:$qdtext|$quotedSingle)*$/";
220 return preg_match($pattern, $string);
224 * Extracts the value from a quoted-string, removing quoted pairs
226 * @param string $value
229 private static function extractQuotedStringValue(string $value): string
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]);