setTimezone(new \DateTimeZone('GMT')); return $dateTime->format('D, d M Y H:i:s \G\M\T'); } /** * This function can be used to aid with content negotiation. * * It takes 2 arguments, the $acceptHeaderValue, which usually comes from * an Accept header, and $availableOptions, which contains an array of * items that the server can support. * * The result of this function will be the 'best possible option'. If no * best possible option could be found, null is returned. * * When it's null you can according to the spec either return a default, or * you can choose to emit 406 Not Acceptable. * * The method also accepts sending 'null' for the $acceptHeaderValue, * implying that no accept header was sent. * * @param string|null $acceptHeaderValue * @param array $availableOptions * @return string|null */ function negotiateContentType($acceptHeaderValue, array $availableOptions) { if (!$acceptHeaderValue) { // Grabbing the first in the list. return reset($availableOptions); } $proposals = array_map( 'Sabre\HTTP\parseMimeType', explode(',', $acceptHeaderValue) ); // Ensuring array keys are reset. $availableOptions = array_values($availableOptions); $options = array_map( 'Sabre\HTTP\parseMimeType', $availableOptions ); $lastQuality = 0; $lastSpecificity = 0; $lastOptionIndex = 0; $lastChoice = null; foreach ($proposals as $proposal) { // Ignoring broken values. if (is_null($proposal)) continue; // If the quality is lower we don't have to bother comparing. if ($proposal['quality'] < $lastQuality) { continue; } foreach ($options as $optionIndex => $option) { if ($proposal['type'] !== '*' && $proposal['type'] !== $option['type']) { // no match on type. continue; } if ($proposal['subType'] !== '*' && $proposal['subType'] !== $option['subType']) { // no match on subtype. continue; } // Any parameters appearing on the options must appear on // proposals. foreach ($option['parameters'] as $paramName => $paramValue) { if (!array_key_exists($paramName, $proposal['parameters'])) { continue 2; } if ($paramValue !== $proposal['parameters'][$paramName]) { continue 2; } } // If we got here, we have a match on parameters, type and // subtype. We need to calculate a score for how specific the // match was. $specificity = ($proposal['type'] !== '*' ? 20 : 0) + ($proposal['subType'] !== '*' ? 10 : 0) + count($option['parameters']); // Does this entry win? if ( ($proposal['quality'] > $lastQuality) || ($proposal['quality'] === $lastQuality && $specificity > $lastSpecificity) || ($proposal['quality'] === $lastQuality && $specificity === $lastSpecificity && $optionIndex < $lastOptionIndex) ) { $lastQuality = $proposal['quality']; $lastSpecificity = $specificity; $lastOptionIndex = $optionIndex; $lastChoice = $availableOptions[$optionIndex]; } } } return $lastChoice; } /** * Parses the Prefer header, as defined in RFC7240. * * Input can be given as a single header value (string) or multiple headers * (array of string). * * This method will return a key->value array with the various Prefer * parameters. * * Prefer: return=minimal will result in: * * [ 'return' => 'minimal' ] * * Prefer: foo, wait=10 will result in: * * [ 'foo' => true, 'wait' => '10'] * * This method also supports the formats from older drafts of RFC7240, and * it will automatically map them to the new values, as the older values * are still pretty common. * * Parameters are currently discarded. There's no known prefer value that * uses them. * * @param string|string[] $input * @return array */ function parsePrefer($input) { $token = '[!#$%&\'*+\-.^_`~A-Za-z0-9]+'; // Work in progress $word = '(?: [a-zA-Z0-9]+ | "[a-zA-Z0-9]*" )'; $regex = << $token) # Prefer property name \s* # Optional space (?: = \s* # Prefer property value (? $word) )? (?: \s* ; (?: .*))? # Prefer parameters (ignored) $ /x REGEX; $output = []; foreach (getHeaderValues($input) as $value) { if (!preg_match($regex, $value, $matches)) { // Ignore continue; } // Mapping old values to their new counterparts switch ($matches['name']) { case 'return-asynch' : $output['respond-async'] = true; break; case 'return-representation' : $output['return'] = 'representation'; break; case 'return-minimal' : $output['return'] = 'minimal'; break; case 'strict' : $output['handling'] = 'strict'; break; case 'lenient' : $output['handling'] = 'lenient'; break; default : if (isset($matches['value'])) { $value = trim($matches['value'], '"'); } else { $value = true; } $output[strtolower($matches['name'])] = empty($value) ? true : $value; break; } } return $output; } /** * This method splits up headers into all their individual values. * * A HTTP header may have more than one header, such as this: * Cache-Control: private, no-store * * Header values are always split with a comma. * * You can pass either a string, or an array. The resulting value is always * an array with each spliced value. * * If the second headers argument is set, this value will simply be merged * in. This makes it quicker to merge an old list of values with a new set. * * @param string|string[] $values * @param string|string[] $values2 * @return string[] */ function getHeaderValues($values, $values2 = null) { $values = (array)$values; if ($values2) { $values = array_merge($values, (array)$values2); } foreach ($values as $l1) { foreach (explode(',', $l1) as $l2) { $result[] = trim($l2); } } return $result; } /** * Parses a mime-type and splits it into: * * 1. type * 2. subtype * 3. quality * 4. parameters * * @param string $str * @return array */ function parseMimeType($str) { $parameters = []; // If no q= parameter appears, then quality = 1. $quality = 1; $parts = explode(';', $str); // The first part is the mime-type. $mimeType = array_shift($parts); $mimeType = explode('/', trim($mimeType)); if (count($mimeType) !== 2) { // Illegal value return null; } list($type, $subType) = $mimeType; foreach ($parts as $part) { $part = trim($part); if (strpos($part, '=')) { list($partName, $partValue) = explode('=', $part, 2); } else { $partName = $part; $partValue = null; } // The quality parameter, if it appears, also marks the end of // the parameter list. Anything after the q= counts as an // 'accept extension' and could introduce new semantics in // content-negotation. if ($partName !== 'q') { $parameters[$partName] = $part; } else { $quality = (float)$partValue; break; // Stop parsing parts } } return [ 'type' => $type, 'subType' => $subType, 'quality' => $quality, 'parameters' => $parameters, ]; } /** * Encodes the path of a url. * * slashes (/) are treated as path-separators. * * @param string $path * @return string */ function encodePath($path) { return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\)\/:@])/', function($match) { return '%' . sprintf('%02x', ord($match[0])); }, $path); } /** * Encodes a 1 segment of a path * * Slashes are considered part of the name, and are encoded as %2f * * @param string $pathSegment * @return string */ function encodePathSegment($pathSegment) { return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\):@])/', function($match) { return '%' . sprintf('%02x', ord($match[0])); }, $pathSegment); } /** * Decodes a url-encoded path * * @param string $path * @return string */ function decodePath($path) { return decodePathSegment($path); } /** * Decodes a url-encoded path segment * * @param string $path * @return string */ function decodePathSegment($path) { $path = rawurldecode($path); $encoding = mb_detect_encoding($path, ['UTF-8', 'ISO-8859-1']); switch ($encoding) { case 'ISO-8859-1' : $path = utf8_encode($path); } return $path; }