Header.php 26 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027
  1. <?php
  2. /**
  3. * The Kohana_HTTP_Header class provides an Object-Orientated interface
  4. * to HTTP headers. This can parse header arrays returned from the
  5. * PHP functions `apache_request_headers()` or the `http_parse_headers()`
  6. * function available within the PECL HTTP library.
  7. *
  8. * @package Kohana
  9. * @category HTTP
  10. * @author Kohana Team
  11. * @since 3.1.0
  12. * @copyright (c) Kohana Team
  13. * @license https://koseven.ga/LICENSE.md
  14. */
  15. class Kohana_HTTP_Header extends ArrayObject {
  16. // Default Accept-* quality value if none supplied
  17. const DEFAULT_QUALITY = 1;
  18. /**
  19. * Parses an Accept(-*) header and detects the quality
  20. *
  21. * @param array $parts accept header parts
  22. * @return array
  23. * @since 3.2.0
  24. */
  25. public static function accept_quality(array $parts)
  26. {
  27. $parsed = [];
  28. // Resource light iteration
  29. $parts_keys = array_keys($parts);
  30. foreach ($parts_keys as $key)
  31. {
  32. $value = trim(str_replace(["\r", "\n"], '', $parts[$key]));
  33. $pattern = '~\b(\;\s*+)?q\s*+=\s*+([.0-9]+)~';
  34. // If there is no quality directive, return default
  35. if ( ! preg_match($pattern, $value, $quality))
  36. {
  37. $parsed[$value] = (float) HTTP_Header::DEFAULT_QUALITY;
  38. }
  39. else
  40. {
  41. $quality = $quality[2];
  42. if ($quality[0] === '.')
  43. {
  44. $quality = '0'.$quality;
  45. }
  46. // Remove the quality value from the string and apply quality
  47. $parsed[trim(preg_replace($pattern, '', $value, 1), '; ')] = (float) $quality;
  48. }
  49. }
  50. return $parsed;
  51. }
  52. /**
  53. * Parses the accept header to provide the correct quality values
  54. * for each supplied accept type.
  55. *
  56. * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
  57. * @param string $accepts accept content header string to parse
  58. * @return array
  59. * @since 3.2.0
  60. */
  61. public static function parse_accept_header($accepts = NULL)
  62. {
  63. $accepts = explode(',', (string) $accepts);
  64. // If there is no accept, lets accept everything
  65. if ($accepts === NULL)
  66. return ['*' => ['*' => (float) HTTP_Header::DEFAULT_QUALITY]];
  67. // Parse the accept header qualities
  68. $accepts = HTTP_Header::accept_quality($accepts);
  69. $parsed_accept = [];
  70. // This method of iteration uses less resource
  71. $keys = array_keys($accepts);
  72. foreach ($keys as $key)
  73. {
  74. // Extract the parts
  75. $parts = explode('/', $key, 2);
  76. // Invalid content type- bail
  77. if ( ! isset($parts[1]))
  78. continue;
  79. // Set the parsed output
  80. $parsed_accept[$parts[0]][$parts[1]] = $accepts[$key];
  81. }
  82. return $parsed_accept;
  83. }
  84. /**
  85. * Parses the `Accept-Charset:` HTTP header and returns an array containing
  86. * the charset and associated quality.
  87. *
  88. * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.2
  89. * @param string $charset charset string to parse
  90. * @return array
  91. * @since 3.2.0
  92. */
  93. public static function parse_charset_header($charset = NULL)
  94. {
  95. if ($charset === NULL)
  96. {
  97. return ['*' => (float) HTTP_Header::DEFAULT_QUALITY];
  98. }
  99. return HTTP_Header::accept_quality(explode(',', (string) $charset));
  100. }
  101. /**
  102. * Parses the `Accept-Encoding:` HTTP header and returns an array containing
  103. * the charsets and associated quality.
  104. *
  105. * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3
  106. * @param string $encoding charset string to parse
  107. * @return array
  108. * @since 3.2.0
  109. */
  110. public static function parse_encoding_header($encoding = NULL)
  111. {
  112. // Accept everything
  113. if ($encoding === NULL)
  114. {
  115. return ['*' => (float) HTTP_Header::DEFAULT_QUALITY];
  116. }
  117. elseif ($encoding === '')
  118. {
  119. return ['identity' => (float) HTTP_Header::DEFAULT_QUALITY];
  120. }
  121. else
  122. {
  123. return HTTP_Header::accept_quality(explode(',', (string) $encoding));
  124. }
  125. }
  126. /**
  127. * Parses the `Accept-Language:` HTTP header and returns an array containing
  128. * the languages and associated quality.
  129. *
  130. * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
  131. * @param string $language charset string to parse
  132. * @return array
  133. * @since 3.2.0
  134. */
  135. public static function parse_language_header($language = NULL)
  136. {
  137. if ($language === NULL)
  138. {
  139. return ['*' => ['*' => (float) HTTP_Header::DEFAULT_QUALITY]];
  140. }
  141. $language = HTTP_Header::accept_quality(explode(',', (string) $language));
  142. $parsed_language = [];
  143. $keys = array_keys($language);
  144. foreach ($keys as $key)
  145. {
  146. // Extract the parts
  147. $parts = explode('-', $key, 2);
  148. // Invalid content type- bail
  149. if ( ! isset($parts[1]))
  150. {
  151. $parsed_language[$parts[0]]['*'] = $language[$key];
  152. }
  153. else
  154. {
  155. // Set the parsed output
  156. $parsed_language[$parts[0]][$parts[1]] = $language[$key];
  157. }
  158. }
  159. return $parsed_language;
  160. }
  161. /**
  162. * Generates a Cache-Control HTTP header based on the supplied array.
  163. *
  164. * // Set the cache control headers you want to use
  165. * $cache_control = array(
  166. * 'max-age' => 3600,
  167. * 'must-revalidate',
  168. * 'public'
  169. * );
  170. *
  171. * // Create the cache control header, creates :
  172. * // cache-control: max-age=3600, must-revalidate, public
  173. * $response->headers('Cache-Control', HTTP_Header::create_cache_control($cache_control);
  174. *
  175. * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13
  176. * @param array $cache_control Cache-Control to render to string
  177. * @return string
  178. */
  179. public static function create_cache_control(array $cache_control)
  180. {
  181. $parts = [];
  182. foreach ($cache_control as $key => $value)
  183. {
  184. $parts[] = (is_int($key)) ? $value : ($key.'='.$value);
  185. }
  186. return implode(', ', $parts);
  187. }
  188. /**
  189. * Parses the Cache-Control header and returning an array representation of the Cache-Control
  190. * header.
  191. *
  192. * // Create the cache control header
  193. * $response->headers('cache-control', 'max-age=3600, must-revalidate, public');
  194. *
  195. * // Parse the cache control header
  196. * if ($cache_control = HTTP_Header::parse_cache_control($response->headers('cache-control')))
  197. * {
  198. * // Cache-Control header was found
  199. * $maxage = $cache_control['max-age'];
  200. * }
  201. *
  202. * @param array $cache_control Array of headers
  203. * @return mixed
  204. */
  205. public static function parse_cache_control($cache_control)
  206. {
  207. $directives = explode(',', strtolower($cache_control));
  208. if ($directives === FALSE)
  209. return FALSE;
  210. $output = [];
  211. foreach ($directives as $directive)
  212. {
  213. if (strpos($directive, '=') !== FALSE)
  214. {
  215. list($key, $value) = explode('=', trim($directive), 2);
  216. $output[$key] = ctype_digit($value) ? (int) $value : $value;
  217. }
  218. else
  219. {
  220. $output[] = trim($directive);
  221. }
  222. }
  223. return $output;
  224. }
  225. /**
  226. * @var array Accept: (content) types
  227. */
  228. protected $_accept_content;
  229. /**
  230. * @var array Accept-Charset: parsed header
  231. */
  232. protected $_accept_charset;
  233. /**
  234. * @var array Accept-Encoding: parsed header
  235. */
  236. protected $_accept_encoding;
  237. /**
  238. * @var array Accept-Language: parsed header
  239. */
  240. protected $_accept_language;
  241. /**
  242. * @var array Accept-Language: language list of parsed header
  243. */
  244. protected $_accept_language_list;
  245. /**
  246. * Constructor method for [Kohana_HTTP_Header]. Uses the standard constructor
  247. * of the parent `ArrayObject` class.
  248. *
  249. * $header_object = new HTTP_Header(array('x-powered-by' => 'Kohana 3.1.x', 'expires' => '...'));
  250. *
  251. * @param mixed $input Input array
  252. * @param int $flags Flags
  253. * @param string $iterator_class The iterator class to use
  254. */
  255. public function __construct(array $input = [], $flags = 0, $iterator_class = 'ArrayIterator')
  256. {
  257. /**
  258. * @link http://www.w3.org/Protocols/rfc2616/rfc2616.html
  259. *
  260. * HTTP header declarations should be treated as case-insensitive
  261. */
  262. $input = array_change_key_case( (array) $input, CASE_LOWER);
  263. parent::__construct($input, $flags, $iterator_class);
  264. }
  265. /**
  266. * Returns the header object as a string, including
  267. * the terminating new line
  268. *
  269. * // Return the header as a string
  270. * echo (string) $request->headers();
  271. *
  272. * @return string
  273. */
  274. public function __toString()
  275. {
  276. $header = '';
  277. foreach ($this as $key => $value)
  278. {
  279. // Put the keys back the Case-Convention expected
  280. $key = Text::ucfirst($key);
  281. if (is_array($value))
  282. {
  283. $header .= $key.': '.(implode(', ', $value))."\r\n";
  284. }
  285. else
  286. {
  287. $header .= $key.': '.$value."\r\n";
  288. }
  289. }
  290. return $header."\r\n";
  291. }
  292. /**
  293. * Overloads `ArrayObject::offsetSet()` to enable handling of header
  294. * with multiple instances of the same directive. If the `$replace` flag
  295. * is `FALSE`, the header will be appended rather than replacing the
  296. * original setting.
  297. *
  298. * @param mixed $index index to set `$newval` to
  299. * @param mixed $newval new value to set
  300. * @param boolean $replace replace existing value
  301. * @return void
  302. * @since 3.2.0
  303. */
  304. #[\ReturnTypeWillChange]
  305. public function offsetSet($index, $newval, $replace = TRUE)
  306. {
  307. // Ensure the index is lowercase
  308. $index = strtolower($index);
  309. if ($replace OR ! $this->offsetExists($index))
  310. {
  311. return parent::offsetSet($index, $newval);
  312. }
  313. $current_value = $this->offsetGet($index);
  314. if (is_array($current_value))
  315. {
  316. $current_value[] = $newval;
  317. }
  318. else
  319. {
  320. $current_value = [$current_value, $newval];
  321. }
  322. return parent::offsetSet($index, $current_value);
  323. }
  324. /**
  325. * Overloads the `ArrayObject::offsetExists()` method to ensure keys
  326. * are lowercase.
  327. *
  328. * @param string $index
  329. * @return boolean
  330. * @since 3.2.0
  331. */
  332. #[\ReturnTypeWillChange]
  333. public function offsetExists($index)
  334. {
  335. return parent::offsetExists(strtolower($index));
  336. }
  337. /**
  338. * Overloads the `ArrayObject::offsetUnset()` method to ensure keys
  339. * are lowercase.
  340. *
  341. * @param string $index
  342. * @return void
  343. * @since 3.2.0
  344. */
  345. #[\ReturnTypeWillChange]
  346. public function offsetUnset($index)
  347. {
  348. return parent::offsetUnset(strtolower($index));
  349. }
  350. /**
  351. * Overload the `ArrayObject::offsetGet()` method to ensure that all
  352. * keys passed to it are formatted correctly for this object.
  353. *
  354. * @param string $index index to retrieve
  355. * @return mixed
  356. * @since 3.2.0
  357. */
  358. #[\ReturnTypeWillChange]
  359. public function offsetGet($index)
  360. {
  361. return parent::offsetGet(strtolower($index));
  362. }
  363. /**
  364. * Overloads the `ArrayObject::exchangeArray()` method to ensure that
  365. * all keys are changed to lowercase.
  366. *
  367. * @param mixed $input
  368. * @return array
  369. * @since 3.2.0
  370. */
  371. #[\ReturnTypeWillChange]
  372. public function exchangeArray($input)
  373. {
  374. /**
  375. * @link http://www.w3.org/Protocols/rfc2616/rfc2616.html
  376. *
  377. * HTTP header declarations should be treated as case-insensitive
  378. */
  379. $input = array_change_key_case( (array) $input, CASE_LOWER);
  380. return parent::exchangeArray($input);
  381. }
  382. /**
  383. * Parses a HTTP Message header line and applies it to this HTTP_Header
  384. *
  385. * $header = $response->headers();
  386. * $header->parse_header_string(NULL, 'content-type: application/json');
  387. *
  388. * @param resource $resource the resource (required by Curl API)
  389. * @param string $header_line the line from the header to parse
  390. * @return int
  391. * @since 3.2.0
  392. */
  393. public function parse_header_string($resource, $header_line)
  394. {
  395. if (preg_match_all('/(\w[^\s:]*):[ ]*([^\r\n]*(?:\r\n[ \t][^\r\n]*)*)/', $header_line, $matches))
  396. {
  397. foreach ($matches[0] as $key => $value)
  398. {
  399. $this->offsetSet($matches[1][$key], $matches[2][$key], FALSE);
  400. }
  401. }
  402. return strlen($header_line);
  403. }
  404. /**
  405. * Returns the accept quality of a submitted mime type based on the
  406. * request `Accept:` header. If the `$explicit` argument is `TRUE`,
  407. * only precise matches will be returned, excluding all wildcard (`*`)
  408. * directives.
  409. *
  410. * // Accept: application/xml; application/json; q=.5; text/html; q=.2, text/*
  411. * // Accept quality for application/json
  412. *
  413. * // $quality = 0.5
  414. * $quality = $request->headers()->accepts_at_quality('application/json');
  415. *
  416. * // $quality_explicit = FALSE
  417. * $quality_explicit = $request->headers()->accepts_at_quality('text/plain', TRUE);
  418. *
  419. * @param string $type
  420. * @param boolean $explicit explicit check, excludes `*`
  421. * @return mixed
  422. * @since 3.2.0
  423. */
  424. public function accepts_at_quality($type, $explicit = FALSE)
  425. {
  426. // Parse Accept header if required
  427. if ($this->_accept_content === NULL)
  428. {
  429. if ($this->offsetExists('Accept'))
  430. {
  431. $accept = $this->offsetGet('Accept');
  432. }
  433. else
  434. {
  435. $accept = '*/*';
  436. }
  437. $this->_accept_content = HTTP_Header::parse_accept_header($accept);
  438. }
  439. // If not a real mime, try and find it in config
  440. if (strpos($type, '/') === FALSE)
  441. {
  442. $mime = Kohana::$config->load('mimes.'.$type);
  443. if ($mime === NULL)
  444. return FALSE;
  445. $quality = FALSE;
  446. foreach ($mime as $_type)
  447. {
  448. $quality_check = $this->accepts_at_quality($_type, $explicit);
  449. $quality = ($quality_check > $quality) ? $quality_check : $quality;
  450. }
  451. return $quality;
  452. }
  453. $parts = explode('/', $type, 2);
  454. if (isset($this->_accept_content[$parts[0]][$parts[1]]))
  455. {
  456. return $this->_accept_content[$parts[0]][$parts[1]];
  457. }
  458. elseif ($explicit === TRUE)
  459. {
  460. return FALSE;
  461. }
  462. else
  463. {
  464. if (isset($this->_accept_content[$parts[0]]['*']))
  465. {
  466. return $this->_accept_content[$parts[0]]['*'];
  467. }
  468. elseif (isset($this->_accept_content['*']['*']))
  469. {
  470. return $this->_accept_content['*']['*'];
  471. }
  472. else
  473. {
  474. return FALSE;
  475. }
  476. }
  477. }
  478. /**
  479. * Returns the preferred response content type based on the accept header
  480. * quality settings. If items have the same quality value, the first item
  481. * found in the array supplied as `$types` will be returned.
  482. *
  483. * // Get the preferred acceptable content type
  484. * // Accept: text/html, application/json; q=.8, text/*
  485. * $result = $header->preferred_accept(array(
  486. * 'text/html'
  487. * 'text/rtf',
  488. * 'application/json'
  489. * )); // $result = 'application/json'
  490. *
  491. * $result = $header->preferred_accept(array(
  492. * 'text/rtf',
  493. * 'application/xml'
  494. * ), TRUE); // $result = FALSE (none matched explicitly)
  495. *
  496. *
  497. * @param array $types the content types to examine
  498. * @param boolean $explicit only allow explicit references, no wildcards
  499. * @return string name of the preferred content type
  500. * @since 3.2.0
  501. */
  502. public function preferred_accept(array $types, $explicit = FALSE)
  503. {
  504. $preferred = FALSE;
  505. $ceiling = 0;
  506. foreach ($types as $type)
  507. {
  508. $quality = $this->accepts_at_quality($type, $explicit);
  509. if ($quality > $ceiling)
  510. {
  511. $preferred = $type;
  512. $ceiling = $quality;
  513. }
  514. }
  515. return $preferred;
  516. }
  517. /**
  518. * Returns the quality of the supplied `$charset` argument. This method
  519. * will automatically parse the `Accept-Charset` header if present and
  520. * return the associated resolved quality value.
  521. *
  522. * // Accept-Charset: utf-8, utf-16; q=.8, iso-8859-1; q=.5
  523. * $quality = $header->accepts_charset_at_quality('utf-8');
  524. * // $quality = (float) 1
  525. *
  526. * @param string $charset charset to examine
  527. * @return float the quality of the charset
  528. * @since 3.2.0
  529. */
  530. public function accepts_charset_at_quality($charset)
  531. {
  532. if ($this->_accept_charset === NULL)
  533. {
  534. if ($this->offsetExists('Accept-Charset'))
  535. {
  536. $charset_header = strtolower($this->offsetGet('Accept-Charset'));
  537. $this->_accept_charset = HTTP_Header::parse_charset_header($charset_header);
  538. }
  539. else
  540. {
  541. $this->_accept_charset = HTTP_Header::parse_charset_header(NULL);
  542. }
  543. }
  544. $charset = strtolower($charset);
  545. if (isset($this->_accept_charset[$charset]))
  546. {
  547. return $this->_accept_charset[$charset];
  548. }
  549. elseif (isset($this->_accept_charset['*']))
  550. {
  551. return $this->_accept_charset['*'];
  552. }
  553. elseif ($charset === 'iso-8859-1')
  554. {
  555. return (float) 1;
  556. }
  557. return (float) 0;
  558. }
  559. /**
  560. * Returns the preferred charset from the supplied array `$charsets` based
  561. * on the `Accept-Charset` header directive.
  562. *
  563. * // Accept-Charset: utf-8, utf-16; q=.8, iso-8859-1; q=.5
  564. * $charset = $header->preferred_charset(array(
  565. * 'utf-10', 'ascii', 'utf-16', 'utf-8'
  566. * )); // $charset = 'utf-8'
  567. *
  568. * @param array $charsets charsets to test
  569. * @return mixed preferred charset or `FALSE`
  570. * @since 3.2.0
  571. */
  572. public function preferred_charset(array $charsets)
  573. {
  574. $preferred = FALSE;
  575. $ceiling = 0;
  576. foreach ($charsets as $charset)
  577. {
  578. $quality = $this->accepts_charset_at_quality($charset);
  579. if ($quality > $ceiling)
  580. {
  581. $preferred = $charset;
  582. $ceiling = $quality;
  583. }
  584. }
  585. return $preferred;
  586. }
  587. /**
  588. * Returns the quality of the `$encoding` type passed to it. Encoding
  589. * is usually compression such as `gzip`, but could be some other
  590. * message encoding algorithm. This method allows explicit checks to be
  591. * done ignoring wildcards.
  592. *
  593. * // Accept-Encoding: compress, gzip, *; q=.5
  594. * $encoding = $header->accepts_encoding_at_quality('gzip');
  595. * // $encoding = (float) 1.0s
  596. *
  597. * @param string $encoding encoding type to interrogate
  598. * @param boolean $explicit explicit check, ignoring wildcards and `identity`
  599. * @return float
  600. * @since 3.2.0
  601. */
  602. public function accepts_encoding_at_quality($encoding, $explicit = FALSE)
  603. {
  604. if ($this->_accept_encoding === NULL)
  605. {
  606. if ($this->offsetExists('Accept-Encoding'))
  607. {
  608. $encoding_header = $this->offsetGet('Accept-Encoding');
  609. }
  610. else
  611. {
  612. $encoding_header = NULL;
  613. }
  614. $this->_accept_encoding = HTTP_Header::parse_encoding_header($encoding_header);
  615. }
  616. // Normalize the encoding
  617. $encoding = strtolower($encoding);
  618. if (isset($this->_accept_encoding[$encoding]))
  619. {
  620. return $this->_accept_encoding[$encoding];
  621. }
  622. if ($explicit === FALSE)
  623. {
  624. if (isset($this->_accept_encoding['*']))
  625. {
  626. return $this->_accept_encoding['*'];
  627. }
  628. elseif ($encoding === 'identity')
  629. {
  630. return (float) HTTP_Header::DEFAULT_QUALITY;
  631. }
  632. }
  633. return (float) 0;
  634. }
  635. /**
  636. * Returns the preferred message encoding type based on quality, and can
  637. * optionally ignore wildcard references. If two or more encodings have the
  638. * same quality, the first listed in `$encodings` will be returned.
  639. *
  640. * // Accept-Encoding: compress, gzip, *; q.5
  641. * $encoding = $header->preferred_encoding(array(
  642. * 'gzip', 'bzip', 'blowfish'
  643. * ));
  644. * // $encoding = 'gzip';
  645. *
  646. * @param array $encodings encodings to test against
  647. * @param boolean $explicit explicit check, if `TRUE` wildcards are excluded
  648. * @return mixed
  649. * @since 3.2.0
  650. */
  651. public function preferred_encoding(array $encodings, $explicit = FALSE)
  652. {
  653. $ceiling = 0;
  654. $preferred = FALSE;
  655. foreach ($encodings as $encoding)
  656. {
  657. $quality = $this->accepts_encoding_at_quality($encoding, $explicit);
  658. if ($quality > $ceiling)
  659. {
  660. $ceiling = $quality;
  661. $preferred = $encoding;
  662. }
  663. }
  664. return $preferred;
  665. }
  666. /**
  667. * Returns the quality of `$language` supplied, optionally ignoring
  668. * wildcards if `$explicit` is set to a non-`FALSE` value. If the quality
  669. * is not found, `0.0` is returned.
  670. *
  671. * // Accept-Language: en-us, en-gb; q=.7, en; q=.5
  672. * $lang = $header->accepts_language_at_quality('en-gb');
  673. * // $lang = (float) 0.7
  674. *
  675. * $lang2 = $header->accepts_language_at_quality('en-au');
  676. * // $lang2 = (float) 0.5
  677. *
  678. * $lang3 = $header->accepts_language_at_quality('en-au', TRUE);
  679. * // $lang3 = (float) 0.0
  680. *
  681. * @param string $language language to interrogate
  682. * @param boolean $explicit explicit interrogation, `TRUE` ignores wildcards
  683. * @return float
  684. * @since 3.2.0
  685. */
  686. public function accepts_language_at_quality($language, $explicit = FALSE)
  687. {
  688. if ($this->_accept_language === NULL)
  689. {
  690. if ($this->offsetExists('Accept-Language'))
  691. {
  692. $language_header = strtolower($this->offsetGet('Accept-Language'));
  693. }
  694. else
  695. {
  696. $language_header = NULL;
  697. }
  698. $this->_accept_language = HTTP_Header::parse_language_header($language_header);
  699. }
  700. // Normalize the language
  701. $language_parts = explode('-', strtolower($language), 2);
  702. if (isset($this->_accept_language[$language_parts[0]]))
  703. {
  704. if (isset($language_parts[1]))
  705. {
  706. if (isset($this->_accept_language[$language_parts[0]][$language_parts[1]]))
  707. {
  708. return $this->_accept_language[$language_parts[0]][$language_parts[1]];
  709. }
  710. elseif ($explicit === FALSE AND isset($this->_accept_language[$language_parts[0]]['*']))
  711. {
  712. return $this->_accept_language[$language_parts[0]]['*'];
  713. }
  714. }
  715. elseif (isset($this->_accept_language[$language_parts[0]]['*']))
  716. {
  717. return $this->_accept_language[$language_parts[0]]['*'];
  718. }
  719. }
  720. if ($explicit === FALSE AND isset($this->_accept_language['*']))
  721. {
  722. return $this->_accept_language['*'];
  723. }
  724. return (float) 0;
  725. }
  726. /**
  727. * Parses the `Accept-Language:` HTTP header and returns an array containing
  728. * the language names.
  729. *
  730. * @param string $language charset string to parse
  731. * @return array
  732. * @since 3.3.8
  733. */
  734. protected static function _parse_language_header_as_list($language = NULL)
  735. {
  736. $languages = [];
  737. $language = explode(',', strtolower($language));
  738. foreach ($language as $lang)
  739. {
  740. $matches = [];
  741. if (preg_match('/([\w-]+)\s*(;.*q.*)?/', $lang, $matches))
  742. {
  743. $languages[] = $matches[1];
  744. }
  745. }
  746. return $languages;
  747. }
  748. /**
  749. * Returns the reordered list of supplied `$languages` using the order
  750. * from the `Accept-Language:` HTTP header.
  751. *
  752. * @param array $languages languages to order
  753. * @param boolean $explicit
  754. * @return array
  755. * @since 3.3.8
  756. */
  757. protected function _order_languages_as_received(array $languages, $explicit = FALSE)
  758. {
  759. if ($this->_accept_language_list === NULL)
  760. {
  761. if ($this->offsetExists('Accept-Language'))
  762. {
  763. $language_header = strtolower($this->offsetGet('Accept-Language'));
  764. }
  765. else
  766. {
  767. $language_header = NULL;
  768. }
  769. $this->_accept_language_list = HTTP_Header::_parse_language_header_as_list($language_header);
  770. }
  771. $new_order = [];
  772. foreach ($this->_accept_language_list as $accept_language)
  773. {
  774. foreach ($languages as $key => $language)
  775. {
  776. if (($explicit AND $accept_language == $language) OR
  777. ( ! $explicit AND substr($accept_language, 0, 2) == substr($language, 0, 2)))
  778. {
  779. $new_order[] = $language;
  780. unset($languages[$key]);
  781. }
  782. }
  783. }
  784. foreach ($languages as $language)
  785. {
  786. $new_order[] = $language;
  787. }
  788. return $new_order;
  789. }
  790. /**
  791. * Returns the preferred language from the supplied array `$languages` based
  792. * on the `Accept-Language` header directive.
  793. *
  794. * // Accept-Language: en-us, en-gb; q=.7, en; q=.5
  795. * $lang = $header->preferred_language(array(
  796. * 'en-gb', 'en-au', 'fr', 'es'
  797. * )); // $lang = 'en-gb'
  798. *
  799. * @param array $languages
  800. * @param boolean $explicit
  801. * @return mixed
  802. * @since 3.2.0
  803. */
  804. public function preferred_language(array $languages, $explicit = FALSE)
  805. {
  806. $ceiling = 0;
  807. $preferred = FALSE;
  808. $languages = $this->_order_languages_as_received($languages, $explicit);
  809. foreach ($languages as $language)
  810. {
  811. $quality = $this->accepts_language_at_quality($language, $explicit);
  812. if ($quality > $ceiling)
  813. {
  814. $ceiling = $quality;
  815. $preferred = $language;
  816. }
  817. }
  818. return $preferred;
  819. }
  820. /**
  821. * Sends headers to the php processor, or supplied `$callback` argument.
  822. * This method formats the headers correctly for output, re-instating their
  823. * capitalization for transmission.
  824. *
  825. * [!!] if you supply a custom header handler via `$callback`, it is
  826. * recommended that `$response` is returned
  827. *
  828. * @param HTTP_Response $response header to send
  829. * @param boolean $replace replace existing value
  830. * @param callback $callback optional callback to replace PHP header function
  831. * @return mixed
  832. * @since 3.2.0
  833. */
  834. public function send_headers(HTTP_Response $response = NULL, $replace = FALSE, $callback = NULL)
  835. {
  836. $protocol = $response->protocol();
  837. $status = $response->status();
  838. // Create the response header
  839. $processed_headers = [$protocol.' '.$status.' '.Response::$messages[$status]];
  840. // Get the headers array
  841. $headers = $response->headers()->getArrayCopy();
  842. foreach ($headers as $header => $value)
  843. {
  844. if (is_array($value))
  845. {
  846. $value = implode(', ', $value);
  847. }
  848. $processed_headers[] = Text::ucfirst($header).': '.$value;
  849. }
  850. if ( ! isset($headers['content-type']))
  851. {
  852. $processed_headers[] = 'Content-Type: '.Kohana::$content_type.'; charset='.Kohana::$charset;
  853. }
  854. if (Kohana::$expose AND ! isset($headers['x-powered-by']))
  855. {
  856. $processed_headers[] = 'X-Powered-By: '.Kohana::version();
  857. }
  858. // Get the cookies and apply
  859. if ($cookies = $response->cookie())
  860. {
  861. $processed_headers['Set-Cookie'] = $cookies;
  862. }
  863. if (is_callable($callback))
  864. {
  865. // Use the callback method to set header
  866. return call_user_func($callback, $response, $processed_headers, $replace);
  867. }
  868. else
  869. {
  870. $this->_send_headers_to_php($processed_headers, $replace);
  871. return $response;
  872. }
  873. }
  874. /**
  875. * Sends the supplied headers to the PHP output buffer. If cookies
  876. * are included in the message they will be handled appropriately.
  877. *
  878. * @param array $headers headers to send to php
  879. * @param boolean $replace replace existing headers
  880. * @return self
  881. * @since 3.2.0
  882. */
  883. protected function _send_headers_to_php(array $headers, $replace)
  884. {
  885. // If the headers have been sent, get out
  886. if (headers_sent())
  887. return $this;
  888. foreach ($headers as $key => $line)
  889. {
  890. if ($key == 'Set-Cookie' AND is_array($line))
  891. {
  892. // Send cookies
  893. foreach ($line as $name => $value)
  894. {
  895. Cookie::set($name, $value['value'], $value['expiration']);
  896. }
  897. continue;
  898. }
  899. header($line, $replace);
  900. }
  901. return $this;
  902. }
  903. }