1<?php 2 3namespace Sabre\HTTP; 4 5use DateTime; 6 7/** 8 * A collection of useful helpers for parsing or generating various HTTP 9 * headers. 10 * 11 * @copyright Copyright (C) 2007-2014 fruux GmbH. All rights reserved. 12 * @author Evert Pot (http://evertpot.com/) 13 * @license http://sabre.io/license/ Modified BSD License 14 */ 15 16/** 17 * Parses a HTTP date-string. 18 * 19 * This method returns false if the date is invalid. 20 * 21 * The following formats are supported: 22 * Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate 23 * Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format 24 * Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format 25 * 26 * See: 27 * http://tools.ietf.org/html/rfc7231#section-7.1.1.1 28 * 29 * @param string $dateString 30 * @return bool|DateTime 31 */ 32function parseDate($dateString) { 33 34 // Only the format is checked, valid ranges are checked by strtotime below 35 $month = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)'; 36 $weekday = '(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)'; 37 $wkday = '(Mon|Tue|Wed|Thu|Fri|Sat|Sun)'; 38 $time = '([0-1]\d|2[0-3])(\:[0-5]\d){2}'; 39 $date3 = $month . ' ([12]\d|3[01]| [1-9])'; 40 $date2 = '(0[1-9]|[12]\d|3[01])\-' . $month . '\-\d{2}'; 41 // 4-digit year cannot begin with 0 - unix timestamp begins in 1970 42 $date1 = '(0[1-9]|[12]\d|3[01]) ' . $month . ' [1-9]\d{3}'; 43 44 // ANSI C's asctime() format 45 // 4-digit year cannot begin with 0 - unix timestamp begins in 1970 46 $asctime_date = $wkday . ' ' . $date3 . ' ' . $time . ' [1-9]\d{3}'; 47 // RFC 850, obsoleted by RFC 1036 48 $rfc850_date = $weekday . ', ' . $date2 . ' ' . $time . ' GMT'; 49 // RFC 822, updated by RFC 1123 50 $rfc1123_date = $wkday . ', ' . $date1 . ' ' . $time . ' GMT'; 51 // allowed date formats by RFC 2616 52 $HTTP_date = "($rfc1123_date|$rfc850_date|$asctime_date)"; 53 54 // allow for space around the string and strip it 55 $dateString = trim($dateString, ' '); 56 if (!preg_match('/^' . $HTTP_date . '$/', $dateString)) 57 return false; 58 59 // append implicit GMT timezone to ANSI C time format 60 if (strpos($dateString, ' GMT') === false) 61 $dateString .= ' GMT'; 62 63 try { 64 return new DateTime($dateString, new \DateTimeZone('UTC')); 65 } catch (\Exception $e) { 66 return false; 67 } 68 69} 70 71/** 72 * Transforms a DateTime object to a valid HTTP/1.1 Date header value 73 * 74 * @param DateTime $dateTime 75 * @return string 76 */ 77function toDate(DateTime $dateTime) { 78 79 // We need to clone it, as we don't want to affect the existing 80 // DateTime. 81 $dateTime = clone $dateTime; 82 $dateTime->setTimeZone(new \DateTimeZone('GMT')); 83 return $dateTime->format('D, d M Y H:i:s \G\M\T'); 84 85} 86 87/** 88 * This function can be used to aid with content negotiation. 89 * 90 * It takes 2 arguments, the $acceptHeaderValue, which usually comes from 91 * an Accept header, and $availableOptions, which contains an array of 92 * items that the server can support. 93 * 94 * The result of this function will be the 'best possible option'. If no 95 * best possible option could be found, null is returned. 96 * 97 * When it's null you can according to the spec either return a default, or 98 * you can choose to emit 406 Not Acceptable. 99 * 100 * The method also accepts sending 'null' for the $acceptHeaderValue, 101 * implying that no accept header was sent. 102 * 103 * @param string|null $acceptHeaderValue 104 * @param array $availableOptions 105 * @return string|null 106 */ 107function negotiateContentType($acceptHeaderValue, array $availableOptions) { 108 109 if (!$acceptHeaderValue) { 110 // Grabbing the first in the list. 111 return reset($availableOptions); 112 } 113 114 $proposals = array_map( 115 'Sabre\HTTP\parseMimeType', 116 explode(',', $acceptHeaderValue) 117 ); 118 119 // Ensuring array keys are reset. 120 $availableOptions = array_values($availableOptions); 121 122 $options = array_map( 123 'Sabre\HTTP\parseMimeType', 124 $availableOptions 125 ); 126 127 $lastQuality = 0; 128 $lastSpecificity = 0; 129 $lastOptionIndex = 0; 130 $lastChoice = null; 131 132 foreach ($proposals as $proposal) { 133 134 // Ignoring broken values. 135 if (is_null($proposal)) continue; 136 137 // If the quality is lower we don't have to bother comparing. 138 if ($proposal['quality'] < $lastQuality) { 139 continue; 140 } 141 142 foreach ($options as $optionIndex => $option) { 143 144 if ($proposal['type'] !== '*' && $proposal['type'] !== $option['type']) { 145 // no match on type. 146 continue; 147 } 148 if ($proposal['subType'] !== '*' && $proposal['subType'] !== $option['subType']) { 149 // no match on subtype. 150 continue; 151 } 152 153 // Any parameters appearing on the options must appear on 154 // proposals. 155 foreach ($option['parameters'] as $paramName => $paramValue) { 156 if (!array_key_exists($paramName, $proposal['parameters'])) { 157 continue 2; 158 } 159 if ($paramValue !== $proposal['parameters'][$paramName]) { 160 continue 2; 161 } 162 } 163 164 // If we got here, we have a match on parameters, type and 165 // subtype. We need to calculate a score for how specific the 166 // match was. 167 $specificity = 168 ($proposal['type'] !== '*' ? 20 : 0) + 169 ($proposal['subType'] !== '*' ? 10 : 0) + 170 count($option['parameters']); 171 172 173 // Does this entry win? 174 if ( 175 ($proposal['quality'] > $lastQuality) || 176 ($proposal['quality'] === $lastQuality && $specificity > $lastSpecificity) || 177 ($proposal['quality'] === $lastQuality && $specificity === $lastSpecificity && $optionIndex < $lastOptionIndex) 178 ) { 179 180 $lastQuality = $proposal['quality']; 181 $lastSpecificity = $specificity; 182 $lastOptionIndex = $optionIndex; 183 $lastChoice = $availableOptions[$optionIndex]; 184 185 } 186 187 } 188 189 } 190 191 return $lastChoice; 192 193} 194 195/** 196 * Parses the Prefer header, as defined in RFC7240. 197 * 198 * Input can be given as a single header value (string) or multiple headers 199 * (array of string). 200 * 201 * This method will return a key->value array with the various Prefer 202 * parameters. 203 * 204 * Prefer: return=minimal will result in: 205 * 206 * [ 'return' => 'minimal' ] 207 * 208 * Prefer: foo, wait=10 will result in: 209 * 210 * [ 'foo' => true, 'wait' => '10'] 211 * 212 * This method also supports the formats from older drafts of RFC7240, and 213 * it will automatically map them to the new values, as the older values 214 * are still pretty common. 215 * 216 * Parameters are currently discarded. There's no known prefer value that 217 * uses them. 218 * 219 * @param string|string[] $header 220 * @return array 221 */ 222function parsePrefer($input) { 223 224 $token = '[!#$%&\'*+\-.^_`~A-Za-z0-9]+'; 225 226 // Work in progress 227 $word = '(?: [a-zA-Z0-9]+ | "[a-zA-Z0-9]*" )'; 228 229 $regex = <<<REGEX 230/ 231^ 232(?<name> $token) # Prefer property name 233\s* # Optional space 234(?: = \s* # Prefer property value 235 (?<value> $word) 236)? 237(?: \s* ; (?: .*))? # Prefer parameters (ignored) 238$ 239/x 240REGEX; 241 242 $output = []; 243 foreach (getHeaderValues($input) as $value) { 244 245 if (!preg_match($regex, $value, $matches)) { 246 // Ignore 247 continue; 248 } 249 250 // Mapping old values to their new counterparts 251 switch ($matches['name']) { 252 case 'return-asynch' : 253 $output['respond-async'] = true; 254 break; 255 case 'return-representation' : 256 $output['return'] = 'representation'; 257 break; 258 case 'return-minimal' : 259 $output['return'] = 'minimal'; 260 break; 261 case 'strict' : 262 $output['handling'] = 'strict'; 263 break; 264 case 'lenient' : 265 $output['handling'] = 'lenient'; 266 break; 267 default : 268 if (isset($matches['value'])) { 269 $value = trim($matches['value'], '"'); 270 } else { 271 $value = true; 272 } 273 $output[strtolower($matches['name'])] = empty($value) ? true : $value; 274 break; 275 } 276 277 } 278 279 return $output; 280 281} 282 283/** 284 * This method splits up headers into all their individual values. 285 * 286 * A HTTP header may have more than one header, such as this: 287 * Cache-Control: private, no-store 288 * 289 * Header values are always split with a comma. 290 * 291 * You can pass either a string, or an array. The resulting value is always 292 * an array with each spliced value. 293 * 294 * If the second headers argument is set, this value will simply be merged 295 * in. This makes it quicker to merge an old list of values with a new set. 296 * 297 * @param string|string[] $values 298 * @param string|string[] $values2 299 * @return string[] 300 */ 301function getHeaderValues($values, $values2 = null) { 302 303 $values = (array)$values; 304 if ($values2) { 305 $values = array_merge($values, (array)$values2); 306 } 307 foreach ($values as $l1) { 308 foreach (explode(',', $l1) as $l2) { 309 $result[] = trim($l2); 310 } 311 } 312 return $result; 313 314} 315 316/** 317 * Parses a mime-type and splits it into: 318 * 319 * 1. type 320 * 2. subtype 321 * 3. quality 322 * 4. parameters 323 * 324 * @param string $str 325 * @return array 326 */ 327function parseMimeType($str) { 328 329 $parameters = []; 330 // If no q= parameter appears, then quality = 1. 331 $quality = 1; 332 333 $parts = explode(';', $str); 334 335 // The first part is the mime-type. 336 $mimeType = array_shift($parts); 337 338 $mimeType = explode('/', trim($mimeType)); 339 if (count($mimeType) !== 2) { 340 // Illegal value 341 return null; 342 } 343 list($type, $subType) = $mimeType; 344 345 foreach ($parts as $part) { 346 347 $part = trim($part); 348 if (strpos($part, '=')) { 349 list($partName, $partValue) = 350 explode('=', $part, 2); 351 } else { 352 $partName = $part; 353 $partValue = null; 354 } 355 356 // The quality parameter, if it appears, also marks the end of 357 // the parameter list. Anything after the q= counts as an 358 // 'accept extension' and could introduce new semantics in 359 // content-negotation. 360 if ($partName !== 'q') { 361 $parameters[$partName] = $part; 362 } else { 363 $quality = (float)$partValue; 364 break; // Stop parsing parts 365 } 366 367 } 368 369 return [ 370 'type' => $type, 371 'subType' => $subType, 372 'quality' => $quality, 373 'parameters' => $parameters, 374 ]; 375 376} 377 378/** 379 * Encodes the path of a url. 380 * 381 * slashes (/) are treated as path-separators. 382 * 383 * @param string $path 384 * @return string 385 */ 386function encodePath($path) { 387 388 return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\)\/:@])/', function($match) { 389 390 return '%' . sprintf('%02x', ord($match[0])); 391 392 }, $path); 393 394} 395 396/** 397 * Encodes a 1 segment of a path 398 * 399 * Slashes are considered part of the name, and are encoded as %2f 400 * 401 * @param string $pathSegment 402 * @return string 403 */ 404function encodePathSegment($pathSegment) { 405 406 return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\):@])/', function($match) { 407 408 return '%' . sprintf('%02x', ord($match[0])); 409 410 }, $pathSegment); 411} 412 413/** 414 * Decodes a url-encoded path 415 * 416 * @param string $path 417 * @return string 418 */ 419function decodePath($path) { 420 421 return decodePathSegment($path); 422 423} 424 425/** 426 * Decodes a url-encoded path segment 427 * 428 * @param string $path 429 * @return string 430 */ 431function decodePathSegment($path) { 432 433 $path = rawurldecode($path); 434 $encoding = mb_detect_encoding($path, ['UTF-8', 'ISO-8859-1']); 435 436 switch ($encoding) { 437 438 case 'ISO-8859-1' : 439 $path = utf8_encode($path); 440 441 } 442 443 return $path; 444 445} 446