PageRenderTime 51ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/system/classes/kohana/response.php

http://github.com/samwilson/kohana_webdb
PHP | 932 lines | 478 code | 110 blank | 344 comment | 52 complexity | 187ac31499e6d68fc7c5266d74347c95 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php defined('SYSPATH') or die('No direct script access.');
  2. /**
  3. * Response wrapper. Created as the result of any [Request] execution
  4. * or utility method (i.e. Redirect). Implements standard HTTP
  5. * response format.
  6. *
  7. * @package Kohana
  8. * @category Base
  9. * @author Kohana Team
  10. * @copyright (c) 2008-2011 Kohana Team
  11. * @license http://kohanaphp.com/license
  12. * @since 3.1.0
  13. */
  14. class Kohana_Response implements Http_Response, Serializable {
  15. /**
  16. * Factory method to create a new [Response]. Pass properties
  17. * in using an associative array.
  18. *
  19. * // Create a new response
  20. * $response = Response::factory();
  21. *
  22. * // Create a new response with headers
  23. * $response = Response::factory(array('status' => 200));
  24. *
  25. * @param array $config Setup the response object
  26. * @return Response
  27. */
  28. public static function factory(array $config = array())
  29. {
  30. return new Response($config);
  31. }
  32. /**
  33. * Generates a [Cache-Control HTTP](http://en.wikipedia.org/wiki/List_of_HTTP_headers)
  34. * header based on the supplied array.
  35. *
  36. * // Set the cache control headers you want to use
  37. * $cache_control = array(
  38. * 'max-age' => 3600,
  39. * 'must-revalidate' => NULL,
  40. * 'public' => NULL
  41. * );
  42. *
  43. * // Create the cache control header, creates :
  44. * // cache-control: max-age=3600, must-revalidate, public
  45. * $response->header['cache-control'] = Response::create_cache_control($cache_control);
  46. *
  47. * @param array $cache_control Cache_control parts to render
  48. * @return string
  49. */
  50. public static function create_cache_control(array $cache_control)
  51. {
  52. // Create a buffer
  53. $parts = array();
  54. // Foreach cache control entry
  55. foreach ($cache_control as $key => $value)
  56. {
  57. // Create a cache control fragment
  58. $parts[] = empty($value) ? $key : ($key.'='.$value);
  59. }
  60. // Return the rendered parts
  61. return implode(', ', $parts);
  62. }
  63. /**
  64. * Parses the Cache-Control header and returning an array representation of the Cache-Control
  65. * header.
  66. *
  67. * // Create the cache control header
  68. * $response->header['cache-control'] = 'max-age=3600, must-revalidate, public';
  69. *
  70. * // Parse the cache control header
  71. * if ($cache_control = Request::parse_cache_control($response->header['cache-control']))
  72. * {
  73. * // Cache-Control header was found
  74. * $maxage = $cache_control['max-age'];
  75. * }
  76. *
  77. * @param array $cache_control Array of headers
  78. * @return mixed
  79. */
  80. public static function parse_cache_control($cache_control)
  81. {
  82. // If no Cache-Control parts are detected
  83. if ( (bool) preg_match_all('/(?<key>[a-z\-]+)=?(?<value>\w+)?/', $cache_control, $matches))
  84. {
  85. // Return combined cache-control key/value pairs
  86. return array_combine($matches['key'], $matches['value']);
  87. }
  88. else
  89. {
  90. // Return
  91. return FALSE;
  92. }
  93. }
  94. // HTTP status codes and messages
  95. public static $messages = array(
  96. // Informational 1xx
  97. 100 => 'Continue',
  98. 101 => 'Switching Protocols',
  99. // Success 2xx
  100. 200 => 'OK',
  101. 201 => 'Created',
  102. 202 => 'Accepted',
  103. 203 => 'Non-Authoritative Information',
  104. 204 => 'No Content',
  105. 205 => 'Reset Content',
  106. 206 => 'Partial Content',
  107. // Redirection 3xx
  108. 300 => 'Multiple Choices',
  109. 301 => 'Moved Permanently',
  110. 302 => 'Found', // 1.1
  111. 303 => 'See Other',
  112. 304 => 'Not Modified',
  113. 305 => 'Use Proxy',
  114. // 306 is deprecated but reserved
  115. 307 => 'Temporary Redirect',
  116. // Client Error 4xx
  117. 400 => 'Bad Request',
  118. 401 => 'Unauthorized',
  119. 402 => 'Payment Required',
  120. 403 => 'Forbidden',
  121. 404 => 'Not Found',
  122. 405 => 'Method Not Allowed',
  123. 406 => 'Not Acceptable',
  124. 407 => 'Proxy Authentication Required',
  125. 408 => 'Request Timeout',
  126. 409 => 'Conflict',
  127. 410 => 'Gone',
  128. 411 => 'Length Required',
  129. 412 => 'Precondition Failed',
  130. 413 => 'Request Entity Too Large',
  131. 414 => 'Request-URI Too Long',
  132. 415 => 'Unsupported Media Type',
  133. 416 => 'Requested Range Not Satisfiable',
  134. 417 => 'Expectation Failed',
  135. // Server Error 5xx
  136. 500 => 'Internal Server Error',
  137. 501 => 'Not Implemented',
  138. 502 => 'Bad Gateway',
  139. 503 => 'Service Unavailable',
  140. 504 => 'Gateway Timeout',
  141. 505 => 'HTTP Version Not Supported',
  142. 509 => 'Bandwidth Limit Exceeded'
  143. );
  144. /**
  145. * @var integer The response http status
  146. */
  147. protected $_status = 200;
  148. /**
  149. * @var Http_Header Headers returned in the response
  150. */
  151. protected $_header;
  152. /**
  153. * @var string The response body
  154. */
  155. protected $_body = '';
  156. /**
  157. * @var array Cookies to be returned in the response
  158. */
  159. protected $_cookies = array();
  160. /**
  161. * @var string The response protocol
  162. */
  163. protected $_protocol;
  164. /**
  165. * Sets up the response object
  166. *
  167. * @param array $config Setup the response object
  168. * @return void
  169. */
  170. public function __construct(array $config = array())
  171. {
  172. $this->_header = new Http_Header(array());
  173. foreach ($config as $key => $value)
  174. {
  175. if (property_exists($this, $key))
  176. {
  177. if ($key == '_header')
  178. {
  179. $this->headers($value);
  180. }
  181. else
  182. {
  183. $this->$key = $value;
  184. }
  185. }
  186. }
  187. }
  188. /**
  189. * Outputs the body when cast to string
  190. *
  191. * @return string
  192. */
  193. public function __toString()
  194. {
  195. return $this->_body;
  196. }
  197. /**
  198. * Gets or sets the body of the response
  199. *
  200. * @return mixed
  201. */
  202. public function body($content = NULL)
  203. {
  204. if ($content)
  205. {
  206. $this->_body = (string) $content;
  207. return $this;
  208. }
  209. return $this->_body;
  210. }
  211. /**
  212. * Gets or sets the HTTP protocol. The standard protocol to use
  213. * is `HTTP/1.1`.
  214. *
  215. * @param string $protocol Protocol to set to the request/response
  216. * @return mixed
  217. */
  218. public function protocol($protocol = NULL)
  219. {
  220. if ($protocol)
  221. {
  222. $this->_protocol = $protocol;
  223. return $this;
  224. }
  225. return $this->_protocol;
  226. }
  227. /**
  228. * Sets or gets the HTTP status from this response.
  229. *
  230. * // Set the HTTP status to 404 Not Found
  231. * $response = Response::factory()
  232. * ->status(404);
  233. *
  234. * // Get the current status
  235. * $status = $response->status();
  236. *
  237. * @param integer $status Status to set to this response
  238. * @return mixed
  239. */
  240. public function status($status = NULL)
  241. {
  242. if ($status === NULL)
  243. {
  244. return $this->_status;
  245. }
  246. elseif (array_key_exists($status, Response::$messages))
  247. {
  248. $this->_status = (int) $status;
  249. return $this;
  250. }
  251. else
  252. {
  253. throw new Kohana_Exception(__METHOD__.' unknown status value : :value', array(':value' => $status));
  254. }
  255. }
  256. /**
  257. * Gets and sets headers to the [Response], allowing chaining
  258. * of response methods. If chaining isn't required, direct
  259. * access to the property should be used instead.
  260. *
  261. * // Get a header
  262. * $accept = $response->headers('Content-Type');
  263. *
  264. * // Set a header
  265. * $response->headers('Content-Type', 'text/html');
  266. *
  267. * // Get all headers
  268. * $headers = $response->headers();
  269. *
  270. * // Set multiple headers
  271. * $response->headers(array('Content-Type' => 'text/html', 'Cache-Control' => 'no-cache'));
  272. *
  273. * @param mixed $key
  274. * @param string $value
  275. * @return mixed
  276. */
  277. public function headers($key = NULL, $value = NULL)
  278. {
  279. if ($key === NULL)
  280. {
  281. return $this->_header;
  282. }
  283. elseif (is_array($key))
  284. {
  285. $this->_header->exchangeArray($key);
  286. return $this;
  287. }
  288. elseif ($value === NULL)
  289. {
  290. return Arr::get($this->_header, $key);
  291. }
  292. else
  293. {
  294. $this->_header[$key] = $value;
  295. return $this;
  296. }
  297. }
  298. /**
  299. * Returns the length of the body for use with
  300. * content header
  301. *
  302. * @return integer
  303. */
  304. public function content_length()
  305. {
  306. return strlen($this->_body);
  307. }
  308. /**
  309. * Set and get cookies values for this response.
  310. *
  311. * // Get the cookies set to the response
  312. * $cookies = $response->cookie();
  313. *
  314. * // Set a cookie to the response
  315. * $response->cookie('session', array(
  316. * 'value' => $value,
  317. * 'expiration' => 12352234
  318. * ));
  319. *
  320. * @param mixed cookie name, or array of cookie values
  321. * @param string value to set to cookie
  322. * @return string
  323. * @return void
  324. * @return [Request]
  325. */
  326. public function cookie($key = NULL, $value = NULL)
  327. {
  328. // Handle the get cookie calls
  329. if ($key === NULL)
  330. return $this->_cookies;
  331. elseif ( ! is_array($key) AND ! $value)
  332. return Arr::get($this->_cookies, $key);
  333. // Handle the set cookie calls
  334. if (is_array($key))
  335. {
  336. reset($key);
  337. while (list($_key, $_value) = each($key))
  338. {
  339. $this->cookie($_key, $_value);
  340. }
  341. }
  342. else
  343. {
  344. if ( ! is_array($value))
  345. {
  346. $value = array(
  347. 'value' => $value,
  348. 'expiration' => Cookie::$expiration
  349. );
  350. }
  351. elseif ( ! isset($value['expiration']))
  352. {
  353. $value['expiration'] = Cookie::$expiration;
  354. }
  355. $this->_cookies[$key] = $value;
  356. }
  357. return $this;
  358. }
  359. /**
  360. * Deletes a cookie set to the response
  361. *
  362. * @param string name
  363. * @return Response
  364. */
  365. public function delete_cookie($name)
  366. {
  367. unset($this->_cookies[$name]);
  368. return $this;
  369. }
  370. /**
  371. * Deletes all cookies from this response
  372. *
  373. * @return Response
  374. */
  375. public function delete_cookies()
  376. {
  377. $this->_cookies = array();
  378. return $this;
  379. }
  380. /**
  381. * Sends the response status and all set headers.
  382. *
  383. * @return Response
  384. */
  385. public function send_headers()
  386. {
  387. if ( ! headers_sent())
  388. {
  389. if (isset($_SERVER['SERVER_PROTOCOL']))
  390. {
  391. // Use the default server protocol
  392. $protocol = $_SERVER['SERVER_PROTOCOL'];
  393. }
  394. else
  395. {
  396. // Default to using newer protocol
  397. $protocol = strtoupper(Http::$protocol).'/'.Http::$version;
  398. }
  399. // Default to text/html; charset=utf8 if no content type set
  400. if ( ! $this->_header->offsetExists('content-type'))
  401. {
  402. $this->_header['content-type'] = Kohana::$content_type.'; charset='.Kohana::$charset;
  403. }
  404. // Add the X-Powered-By header
  405. if (Kohana::$expose)
  406. {
  407. $this->_header['x-powered-by'] = 'Kohana Framework '.Kohana::VERSION.' ('.Kohana::CODENAME.')';
  408. }
  409. // HTTP status line
  410. header($protocol.' '.$this->_status.' '.Response::$messages[$this->_status]);
  411. foreach ($this->_header as $name => $value)
  412. {
  413. if (is_string($name))
  414. {
  415. // Combine the name and value to make a raw header
  416. $value = $name.': '.$value;
  417. }
  418. // Send the raw header
  419. header($value, TRUE);
  420. }
  421. // Send cookies
  422. foreach ($this->_cookies as $name => $value)
  423. {
  424. Cookie::set($name, $value['value'], $value['expiration']);
  425. }
  426. }
  427. return $this;
  428. }
  429. /**
  430. * Send file download as the response. All execution will be halted when
  431. * this method is called! Use TRUE for the filename to send the current
  432. * response as the file content. The third parameter allows the following
  433. * options to be set:
  434. *
  435. * Type | Option | Description | Default Value
  436. * ----------|-----------|------------------------------------|--------------
  437. * `boolean` | inline | Display inline instead of download | `FALSE`
  438. * `string` | mime_type | Manual mime type | Automatic
  439. * `boolean` | delete | Delete the file after sending | `FALSE`
  440. *
  441. * Download a file that already exists:
  442. *
  443. * $request->send_file('media/packages/kohana.zip');
  444. *
  445. * Download generated content as a file:
  446. *
  447. * $request->response($content);
  448. * $request->send_file(TRUE, $filename);
  449. *
  450. * [!!] No further processing can be done after this method is called!
  451. *
  452. * @param string filename with path, or TRUE for the current response
  453. * @param string downloaded file name
  454. * @param array additional options
  455. * @return void
  456. * @throws Kohana_Exception
  457. * @uses File::mime_by_ext
  458. * @uses File::mime
  459. * @uses Request::send_headers
  460. */
  461. public function send_file($filename, $download = NULL, array $options = NULL)
  462. {
  463. if ( ! empty($options['mime_type']))
  464. {
  465. // The mime-type has been manually set
  466. $mime = $options['mime_type'];
  467. }
  468. if ($filename === TRUE)
  469. {
  470. if (empty($download))
  471. {
  472. throw new Kohana_Exception('Download name must be provided for streaming files');
  473. }
  474. // Temporary files will automatically be deleted
  475. $options['delete'] = FALSE;
  476. if ( ! isset($mime))
  477. {
  478. // Guess the mime using the file extension
  479. $mime = File::mime_by_ext(strtolower(pathinfo($download, PATHINFO_EXTENSION)));
  480. }
  481. // Force the data to be rendered if
  482. $file_data = (string) $this->_body;
  483. // Get the content size
  484. $size = strlen($file_data);
  485. // Create a temporary file to hold the current response
  486. $file = tmpfile();
  487. // Write the current response into the file
  488. fwrite($file, $file_data);
  489. // File data is no longer needed
  490. unset($file_data);
  491. }
  492. else
  493. {
  494. // Get the complete file path
  495. $filename = realpath($filename);
  496. if (empty($download))
  497. {
  498. // Use the file name as the download file name
  499. $download = pathinfo($filename, PATHINFO_BASENAME);
  500. }
  501. // Get the file size
  502. $size = filesize($filename);
  503. if ( ! isset($mime))
  504. {
  505. // Get the mime type
  506. $mime = File::mime($filename);
  507. }
  508. // Open the file for reading
  509. $file = fopen($filename, 'rb');
  510. }
  511. if ( ! is_resource($file))
  512. {
  513. throw new Kohana_Exception('Could not read file to send: :file', array(
  514. ':file' => $download,
  515. ));
  516. }
  517. // Inline or download?
  518. $disposition = empty($options['inline']) ? 'attachment' : 'inline';
  519. // Calculate byte range to download.
  520. list($start, $end) = $this->_calculate_byte_range($size);
  521. if ( ! empty($options['resumable']))
  522. {
  523. if ($start > 0 OR $end < ($size - 1))
  524. {
  525. // Partial Content
  526. $this->_status = 206;
  527. }
  528. // Range of bytes being sent
  529. $this->_header['content-range'] = 'bytes '.$start.'-'.$end.'/'.$size;
  530. $this->_header['accept-ranges'] = 'bytes';
  531. }
  532. // Set the headers for a download
  533. $this->_header['content-disposition'] = $disposition.'; filename="'.$download.'"';
  534. $this->_header['content-type'] = $mime;
  535. $this->_header['content-length'] = (string) (($end - $start) + 1);
  536. if (Request::user_agent('browser') === 'Internet Explorer')
  537. {
  538. // Naturally, IE does not act like a real browser...
  539. if (Request::$initial->protocol() === 'https')
  540. {
  541. // http://support.microsoft.com/kb/316431
  542. $this->_header['pragma'] = $this->_header['cache-control'] = 'public';
  543. }
  544. if (version_compare(Request::user_agent('version'), '8.0', '>='))
  545. {
  546. // http://ajaxian.com/archives/ie-8-security
  547. $this->_header['x-content-type-options'] = 'nosniff';
  548. }
  549. }
  550. // Send all headers now
  551. $this->send_headers();
  552. while (ob_get_level())
  553. {
  554. // Flush all output buffers
  555. ob_end_flush();
  556. }
  557. // Manually stop execution
  558. ignore_user_abort(TRUE);
  559. if ( ! Kohana::$safe_mode)
  560. {
  561. // Keep the script running forever
  562. set_time_limit(0);
  563. }
  564. // Send data in 16kb blocks
  565. $block = 1024 * 16;
  566. fseek($file, $start);
  567. while ( ! feof($file) AND ($pos = ftell($file)) <= $end)
  568. {
  569. if (connection_aborted())
  570. break;
  571. if ($pos + $block > $end)
  572. {
  573. // Don't read past the buffer.
  574. $block = $end - $pos + 1;
  575. }
  576. // Output a block of the file
  577. echo fread($file, $block);
  578. // Send the data now
  579. flush();
  580. }
  581. // Close the file
  582. fclose($file);
  583. if ( ! empty($options['delete']))
  584. {
  585. try
  586. {
  587. // Attempt to remove the file
  588. unlink($filename);
  589. }
  590. catch (Exception $e)
  591. {
  592. // Create a text version of the exception
  593. $error = Kohana_Exception::text($e);
  594. if (is_object(Kohana::$log))
  595. {
  596. // Add this exception to the log
  597. Kohana::$log->add(Log::ERROR, $error);
  598. // Make sure the logs are written
  599. Kohana::$log->write();
  600. }
  601. // Do NOT display the exception, it will corrupt the output!
  602. }
  603. }
  604. // Stop execution
  605. exit;
  606. }
  607. /**
  608. * Renders the Http_Interaction to a string, producing
  609. *
  610. * - Protocol
  611. * - Headers
  612. * - Body
  613. *
  614. * @return string
  615. */
  616. public function render()
  617. {
  618. if ( ! $this->_header->offsetExists('content-type'))
  619. {
  620. // Add the default Content-Type header if required
  621. $this->_header['content-type'] = Kohana::$content_type.'; charset='.Kohana::$charset;
  622. }
  623. $content_length = $this->content_length();
  624. // Set the content length for the body if required
  625. if ($content_length > 0)
  626. {
  627. $this->_header['content-length'] = (string) $content_length;
  628. }
  629. // Prepare cookies
  630. if ($this->_cookies)
  631. {
  632. if (extension_loaded('http'))
  633. {
  634. $this->_header['set-cookie'] = http_build_cookie($this->_cookies);
  635. }
  636. else
  637. {
  638. $cookies = array();
  639. // Parse each
  640. foreach ($this->_cookies as $key => $value)
  641. {
  642. $string = $key.'='.$value['value'].'; expires='.date('l, d M Y H:i:s T', $value['expiration']);
  643. $cookies[] = $string;
  644. }
  645. // Create the cookie string
  646. $this->_header['set-cookie'] = $cookies;
  647. }
  648. }
  649. $output = $this->_protocol.' '.$this->_status.' '.Response::$messages[$this->_status]."\n";
  650. $output .= (string) $this->_header;
  651. $output .= $this->_body;
  652. return $output;
  653. }
  654. /**
  655. * Generate ETag
  656. * Generates an ETag from the response ready to be returned
  657. *
  658. * @throws Kohana_Request_Exception
  659. * @return String Generated ETag
  660. */
  661. public function generate_etag()
  662. {
  663. if ($this->_body === NULL)
  664. {
  665. throw new Kohana_Request_Exception('No response yet associated with request - cannot auto generate resource ETag');
  666. }
  667. // Generate a unique hash for the response
  668. return '"'.sha1($this->render()).'"';
  669. }
  670. /**
  671. * Check Cache
  672. * Checks the browser cache to see the response needs to be returned
  673. *
  674. * @param string $etag Resource ETag
  675. * @param Request $request The request to test against
  676. * @return Response
  677. * @throws Kohana_Request_Exception
  678. */
  679. public function check_cache($etag = NULL, Request $request = NULL)
  680. {
  681. if ( ! $etag)
  682. {
  683. $etag = $this->generate_etag();
  684. }
  685. if ( ! $request)
  686. throw new Kohana_Request_Exception('A Request object must be supplied with an etag for evaluation');
  687. // Set the ETag header
  688. $this->_header['etag'] = $etag;
  689. // Add the Cache-Control header if it is not already set
  690. // This allows etags to be used with max-age, etc
  691. if ($this->_header->offsetExists('cache-control'))
  692. {
  693. if (is_array($this->_header['cache-control']))
  694. {
  695. $this->_header['cache-control'][] = new Http_Header_Value('must-revalidate');
  696. }
  697. else
  698. {
  699. $this->_header['cache-control'] = $this->_header['cache-control'].', must-revalidate';
  700. }
  701. }
  702. else
  703. {
  704. $this->_header['cache-control'] = 'must-revalidate';
  705. }
  706. if ($request->headers('if-none-match') AND (string) $request->headers('if-none-match') === $etag)
  707. {
  708. // No need to send data again
  709. $this->_status = 304;
  710. $this->send_headers();
  711. // Stop execution
  712. exit;
  713. }
  714. return $this;
  715. }
  716. /**
  717. * Serializes the object to json - handy if you
  718. * need to pass the response data to other
  719. * systems
  720. *
  721. * @param array array of data to serialize
  722. * @return string
  723. * @throws Kohana_Exception
  724. */
  725. public function serialize(array $to_serialize = array())
  726. {
  727. // Serialize the class properties
  728. $to_serialize += array
  729. (
  730. '_status' => $this->_status,
  731. '_header' => $this->_header,
  732. '_cookies' => $this->_cookies,
  733. '_body' => $this->_body
  734. );
  735. $serialized = serialize($to_serialize);
  736. if (is_string($serialized))
  737. {
  738. return $string;
  739. }
  740. else
  741. {
  742. throw new Kohana_Exception('Unable to serialize object');
  743. }
  744. }
  745. /**
  746. * JSON encoded object
  747. *
  748. * @param string json encoded object
  749. * @return bool
  750. * @throws Kohana_Exception
  751. */
  752. public function unserialize($string)
  753. {
  754. // Unserialise object
  755. $unserialized = unserialize($string);
  756. // If failed
  757. if ($unserialized === NULL)
  758. {
  759. // Throw exception
  760. throw new Kohana_Exception('Unable to correctly unserialize string: :string', array(':string' => $string));
  761. }
  762. // Foreach key/value pair
  763. foreach ($unserialized as $key => $value)
  764. {
  765. $this->$key = $value;
  766. }
  767. return TRUE;
  768. }
  769. /**
  770. * Parse the byte ranges from the HTTP_RANGE header used for
  771. * resumable downloads.
  772. *
  773. * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
  774. * @return array|FALSE
  775. */
  776. protected function _parse_byte_range()
  777. {
  778. if ( ! isset($_SERVER['HTTP_RANGE']))
  779. {
  780. return FALSE;
  781. }
  782. // TODO, speed this up with the use of string functions.
  783. preg_match_all('/(-?[0-9]++(?:-(?![0-9]++))?)(?:-?([0-9]++))?/', $_SERVER['HTTP_RANGE'], $matches, PREG_SET_ORDER);
  784. return $matches[0];
  785. }
  786. /**
  787. * Calculates the byte range to use with send_file. If HTTP_RANGE doesn't
  788. * exist then the complete byte range is returned
  789. *
  790. * @param integer $size
  791. * @return array
  792. */
  793. protected function _calculate_byte_range($size)
  794. {
  795. // Defaults to start with when the HTTP_RANGE header doesn't exist.
  796. $start = 0;
  797. $end = $size - 1;
  798. if ($range = $this->_parse_byte_range())
  799. {
  800. // We have a byte range from HTTP_RANGE
  801. $start = $range[1];
  802. if ($start[0] === '-')
  803. {
  804. // A negative value means we start from the end, so -500 would be the
  805. // last 500 bytes.
  806. $start = $size - abs($start);
  807. }
  808. if (isset($range[2]))
  809. {
  810. // Set the end range
  811. $end = $range[2];
  812. }
  813. }
  814. // Normalize values.
  815. $start = abs(intval($start));
  816. // Keep the the end value in bounds and normalize it.
  817. $end = min(abs(intval($end)), $size - 1);
  818. // Keep the start in bounds.
  819. $start = ($end < $start) ? 0 : max($start, 0);
  820. return array($start, $end);
  821. }
  822. } // End Kohana_Response