PageRenderTime 50ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/system/classes/kohana/response.php

https://bitbucket.org/rlm3/mrs
PHP | 933 lines | 479 code | 110 blank | 344 comment | 53 complexity | 27fdd7bd043f1ec75c6a19e745787209 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 === NULL)
  205. return $this->_body;
  206. $this->_body = (string) $content;
  207. return $this;
  208. }
  209. /**
  210. * Gets or sets the HTTP protocol. The standard protocol to use
  211. * is `HTTP/1.1`.
  212. *
  213. * @param string $protocol Protocol to set to the request/response
  214. * @return mixed
  215. */
  216. public function protocol($protocol = NULL)
  217. {
  218. if ($protocol)
  219. {
  220. $this->_protocol = $protocol;
  221. return $this;
  222. }
  223. return $this->_protocol;
  224. }
  225. /**
  226. * Sets or gets the HTTP status from this response.
  227. *
  228. * // Set the HTTP status to 404 Not Found
  229. * $response = Response::factory()
  230. * ->status(404);
  231. *
  232. * // Get the current status
  233. * $status = $response->status();
  234. *
  235. * @param integer $status Status to set to this response
  236. * @return mixed
  237. */
  238. public function status($status = NULL)
  239. {
  240. if ($status === NULL)
  241. {
  242. return $this->_status;
  243. }
  244. elseif (array_key_exists($status, Response::$messages))
  245. {
  246. $this->_status = (int) $status;
  247. return $this;
  248. }
  249. else
  250. {
  251. throw new Kohana_Exception(__METHOD__.' unknown status value : :value', array(':value' => $status));
  252. }
  253. }
  254. /**
  255. * Gets and sets headers to the [Response], allowing chaining
  256. * of response methods. If chaining isn't required, direct
  257. * access to the property should be used instead.
  258. *
  259. * // Get a header
  260. * $accept = $response->headers('Content-Type');
  261. *
  262. * // Set a header
  263. * $response->headers('Content-Type', 'text/html');
  264. *
  265. * // Get all headers
  266. * $headers = $response->headers();
  267. *
  268. * // Set multiple headers
  269. * $response->headers(array('Content-Type' => 'text/html', 'Cache-Control' => 'no-cache'));
  270. *
  271. * @param mixed $key
  272. * @param string $value
  273. * @return mixed
  274. */
  275. public function headers($key = NULL, $value = NULL)
  276. {
  277. if ($key === NULL)
  278. {
  279. return $this->_header;
  280. }
  281. elseif (is_array($key))
  282. {
  283. $this->_header->exchangeArray($key);
  284. return $this;
  285. }
  286. elseif ($value === NULL)
  287. {
  288. return Arr::get($this->_header, $key);
  289. }
  290. else
  291. {
  292. $this->_header[$key] = $value;
  293. return $this;
  294. }
  295. }
  296. /**
  297. * Returns the length of the body for use with
  298. * content header
  299. *
  300. * @return integer
  301. */
  302. public function content_length()
  303. {
  304. return strlen($this->_body);
  305. }
  306. /**
  307. * Set and get cookies values for this response.
  308. *
  309. * // Get the cookies set to the response
  310. * $cookies = $response->cookie();
  311. *
  312. * // Set a cookie to the response
  313. * $response->cookie('session', array(
  314. * 'value' => $value,
  315. * 'expiration' => 12352234
  316. * ));
  317. *
  318. * @param mixed cookie name, or array of cookie values
  319. * @param string value to set to cookie
  320. * @return string
  321. * @return void
  322. * @return [Response]
  323. */
  324. public function cookie($key = NULL, $value = NULL)
  325. {
  326. // Handle the get cookie calls
  327. if ($key === NULL)
  328. return $this->_cookies;
  329. elseif ( ! is_array($key) AND ! $value)
  330. return Arr::get($this->_cookies, $key);
  331. // Handle the set cookie calls
  332. if (is_array($key))
  333. {
  334. reset($key);
  335. while (list($_key, $_value) = each($key))
  336. {
  337. $this->cookie($_key, $_value);
  338. }
  339. }
  340. else
  341. {
  342. if ( ! is_array($value))
  343. {
  344. $value = array(
  345. 'value' => $value,
  346. 'expiration' => Cookie::$expiration
  347. );
  348. }
  349. elseif ( ! isset($value['expiration']))
  350. {
  351. $value['expiration'] = Cookie::$expiration;
  352. }
  353. $this->_cookies[$key] = $value;
  354. }
  355. return $this;
  356. }
  357. /**
  358. * Deletes a cookie set to the response
  359. *
  360. * @param string name
  361. * @return Response
  362. */
  363. public function delete_cookie($name)
  364. {
  365. unset($this->_cookies[$name]);
  366. return $this;
  367. }
  368. /**
  369. * Deletes all cookies from this response
  370. *
  371. * @return Response
  372. */
  373. public function delete_cookies()
  374. {
  375. $this->_cookies = array();
  376. return $this;
  377. }
  378. /**
  379. * Sends the response status and all set headers.
  380. *
  381. * @return Response
  382. */
  383. public function send_headers()
  384. {
  385. if ( ! headers_sent())
  386. {
  387. if (isset($_SERVER['SERVER_PROTOCOL']))
  388. {
  389. // Use the default server protocol
  390. $protocol = $_SERVER['SERVER_PROTOCOL'];
  391. }
  392. else
  393. {
  394. // Default to using newer protocol
  395. $protocol = strtoupper(HTTP::$protocol).'/'.HTTP::$version;
  396. }
  397. // Default to text/html; charset=utf8 if no content type set
  398. if ( ! $this->_header->offsetExists('content-type'))
  399. {
  400. $this->_header['content-type'] = Kohana::$content_type.'; charset='.Kohana::$charset;
  401. }
  402. // Add the X-Powered-By header
  403. if (Kohana::$expose)
  404. {
  405. $this->_header['x-powered-by'] = 'Kohana Framework '.Kohana::VERSION.' ('.Kohana::CODENAME.')';
  406. }
  407. if ( ! Kohana::$is_cli)
  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. }
  422. // Send cookies
  423. foreach ($this->_cookies as $name => $value)
  424. {
  425. Cookie::set($name, $value['value'], $value['expiration']);
  426. }
  427. }
  428. return $this;
  429. }
  430. /**
  431. * Send file download as the response. All execution will be halted when
  432. * this method is called! Use TRUE for the filename to send the current
  433. * response as the file content. The third parameter allows the following
  434. * options to be set:
  435. *
  436. * Type | Option | Description | Default Value
  437. * ----------|-----------|------------------------------------|--------------
  438. * `boolean` | inline | Display inline instead of download | `FALSE`
  439. * `string` | mime_type | Manual mime type | Automatic
  440. * `boolean` | delete | Delete the file after sending | `FALSE`
  441. *
  442. * Download a file that already exists:
  443. *
  444. * $request->send_file('media/packages/kohana.zip');
  445. *
  446. * Download generated content as a file:
  447. *
  448. * $request->response($content);
  449. * $request->send_file(TRUE, $filename);
  450. *
  451. * [!!] No further processing can be done after this method is called!
  452. *
  453. * @param string filename with path, or TRUE for the current response
  454. * @param string downloaded file name
  455. * @param array additional options
  456. * @return void
  457. * @throws Kohana_Exception
  458. * @uses File::mime_by_ext
  459. * @uses File::mime
  460. * @uses Request::send_headers
  461. */
  462. public function send_file($filename, $download = NULL, array $options = NULL)
  463. {
  464. if ( ! empty($options['mime_type']))
  465. {
  466. // The mime-type has been manually set
  467. $mime = $options['mime_type'];
  468. }
  469. if ($filename === TRUE)
  470. {
  471. if (empty($download))
  472. {
  473. throw new Kohana_Exception('Download name must be provided for streaming files');
  474. }
  475. // Temporary files will automatically be deleted
  476. $options['delete'] = FALSE;
  477. if ( ! isset($mime))
  478. {
  479. // Guess the mime using the file extension
  480. $mime = File::mime_by_ext(strtolower(pathinfo($download, PATHINFO_EXTENSION)));
  481. }
  482. // Force the data to be rendered if
  483. $file_data = (string) $this->_body;
  484. // Get the content size
  485. $size = strlen($file_data);
  486. // Create a temporary file to hold the current response
  487. $file = tmpfile();
  488. // Write the current response into the file
  489. fwrite($file, $file_data);
  490. // File data is no longer needed
  491. unset($file_data);
  492. }
  493. else
  494. {
  495. // Get the complete file path
  496. $filename = realpath($filename);
  497. if (empty($download))
  498. {
  499. // Use the file name as the download file name
  500. $download = pathinfo($filename, PATHINFO_BASENAME);
  501. }
  502. // Get the file size
  503. $size = filesize($filename);
  504. if ( ! isset($mime))
  505. {
  506. // Get the mime type
  507. $mime = File::mime($filename);
  508. }
  509. // Open the file for reading
  510. $file = fopen($filename, 'rb');
  511. }
  512. if ( ! is_resource($file))
  513. {
  514. throw new Kohana_Exception('Could not read file to send: :file', array(
  515. ':file' => $download,
  516. ));
  517. }
  518. // Inline or download?
  519. $disposition = empty($options['inline']) ? 'attachment' : 'inline';
  520. // Calculate byte range to download.
  521. list($start, $end) = $this->_calculate_byte_range($size);
  522. if ( ! empty($options['resumable']))
  523. {
  524. if ($start > 0 OR $end < ($size - 1))
  525. {
  526. // Partial Content
  527. $this->_status = 206;
  528. }
  529. // Range of bytes being sent
  530. $this->_header['content-range'] = 'bytes '.$start.'-'.$end.'/'.$size;
  531. $this->_header['accept-ranges'] = 'bytes';
  532. }
  533. // Set the headers for a download
  534. $this->_header['content-disposition'] = $disposition.'; filename="'.$download.'"';
  535. $this->_header['content-type'] = $mime;
  536. $this->_header['content-length'] = (string) (($end - $start) + 1);
  537. if (Request::user_agent('browser') === 'Internet Explorer')
  538. {
  539. // Naturally, IE does not act like a real browser...
  540. if (Request::$initial->protocol() === 'https')
  541. {
  542. // http://support.microsoft.com/kb/316431
  543. $this->_header['pragma'] = $this->_header['cache-control'] = 'public';
  544. }
  545. if (version_compare(Request::user_agent('version'), '8.0', '>='))
  546. {
  547. // http://ajaxian.com/archives/ie-8-security
  548. $this->_header['x-content-type-options'] = 'nosniff';
  549. }
  550. }
  551. // Send all headers now
  552. $this->send_headers();
  553. while (ob_get_level())
  554. {
  555. // Flush all output buffers
  556. ob_end_flush();
  557. }
  558. // Manually stop execution
  559. ignore_user_abort(TRUE);
  560. if ( ! Kohana::$safe_mode)
  561. {
  562. // Keep the script running forever
  563. set_time_limit(0);
  564. }
  565. // Send data in 16kb blocks
  566. $block = 1024 * 16;
  567. fseek($file, $start);
  568. while ( ! feof($file) AND ($pos = ftell($file)) <= $end)
  569. {
  570. if (connection_aborted())
  571. break;
  572. if ($pos + $block > $end)
  573. {
  574. // Don't read past the buffer.
  575. $block = $end - $pos + 1;
  576. }
  577. // Output a block of the file
  578. echo fread($file, $block);
  579. // Send the data now
  580. flush();
  581. }
  582. // Close the file
  583. fclose($file);
  584. if ( ! empty($options['delete']))
  585. {
  586. try
  587. {
  588. // Attempt to remove the file
  589. unlink($filename);
  590. }
  591. catch (Exception $e)
  592. {
  593. // Create a text version of the exception
  594. $error = Kohana_Exception::text($e);
  595. if (is_object(Kohana::$log))
  596. {
  597. // Add this exception to the log
  598. Kohana::$log->add(Log::ERROR, $error);
  599. // Make sure the logs are written
  600. Kohana::$log->write();
  601. }
  602. // Do NOT display the exception, it will corrupt the output!
  603. }
  604. }
  605. // Stop execution
  606. exit;
  607. }
  608. /**
  609. * Renders the HTTP_Interaction to a string, producing
  610. *
  611. * - Protocol
  612. * - Headers
  613. * - Body
  614. *
  615. * @return string
  616. */
  617. public function render()
  618. {
  619. if ( ! $this->_header->offsetExists('content-type'))
  620. {
  621. // Add the default Content-Type header if required
  622. $this->_header['content-type'] = Kohana::$content_type.'; charset='.Kohana::$charset;
  623. }
  624. $content_length = $this->content_length();
  625. // Set the content length for the body if required
  626. if ($content_length > 0)
  627. {
  628. $this->_header['content-length'] = (string) $content_length;
  629. }
  630. // Prepare cookies
  631. if ($this->_cookies)
  632. {
  633. if (extension_loaded('http'))
  634. {
  635. $this->_header['set-cookie'] = http_build_cookie($this->_cookies);
  636. }
  637. else
  638. {
  639. $cookies = array();
  640. // Parse each
  641. foreach ($this->_cookies as $key => $value)
  642. {
  643. $string = $key.'='.$value['value'].'; expires='.date('l, d M Y H:i:s T', $value['expiration']);
  644. $cookies[] = $string;
  645. }
  646. // Create the cookie string
  647. $this->_header['set-cookie'] = $cookies;
  648. }
  649. }
  650. $output = $this->_protocol.' '.$this->_status.' '.Response::$messages[$this->_status]."\n";
  651. $output .= (string) $this->_header;
  652. $output .= $this->_body;
  653. return $output;
  654. }
  655. /**
  656. * Generate ETag
  657. * Generates an ETag from the response ready to be returned
  658. *
  659. * @throws Kohana_Request_Exception
  660. * @return String Generated ETag
  661. */
  662. public function generate_etag()
  663. {
  664. if ($this->_body === NULL)
  665. {
  666. throw new Kohana_Request_Exception('No response yet associated with request - cannot auto generate resource ETag');
  667. }
  668. // Generate a unique hash for the response
  669. return '"'.sha1($this->render()).'"';
  670. }
  671. /**
  672. * Check Cache
  673. * Checks the browser cache to see the response needs to be returned
  674. *
  675. * @param string $etag Resource ETag
  676. * @param Request $request The request to test against
  677. * @return Response
  678. * @throws Kohana_Request_Exception
  679. */
  680. public function check_cache($etag = NULL, Request $request = NULL)
  681. {
  682. if ( ! $etag)
  683. {
  684. $etag = $this->generate_etag();
  685. }
  686. if ( ! $request)
  687. throw new Kohana_Request_Exception('A Request object must be supplied with an etag for evaluation');
  688. // Set the ETag header
  689. $this->_header['etag'] = $etag;
  690. // Add the Cache-Control header if it is not already set
  691. // This allows etags to be used with max-age, etc
  692. if ($this->_header->offsetExists('cache-control'))
  693. {
  694. if (is_array($this->_header['cache-control']))
  695. {
  696. $this->_header['cache-control'][] = new HTTP_Header_Value('must-revalidate');
  697. }
  698. else
  699. {
  700. $this->_header['cache-control'] = $this->_header['cache-control'].', must-revalidate';
  701. }
  702. }
  703. else
  704. {
  705. $this->_header['cache-control'] = 'must-revalidate';
  706. }
  707. if ($request->headers('if-none-match') AND (string) $request->headers('if-none-match') === $etag)
  708. {
  709. // No need to send data again
  710. $this->_status = 304;
  711. $this->send_headers();
  712. // Stop execution
  713. exit;
  714. }
  715. return $this;
  716. }
  717. /**
  718. * Serializes the object to json - handy if you
  719. * need to pass the response data to other
  720. * systems
  721. *
  722. * @param array array of data to serialize
  723. * @return string
  724. * @throws Kohana_Exception
  725. */
  726. public function serialize(array $to_serialize = array())
  727. {
  728. // Serialize the class properties
  729. $to_serialize += array
  730. (
  731. '_status' => $this->_status,
  732. '_header' => $this->_header,
  733. '_cookies' => $this->_cookies,
  734. '_body' => $this->_body
  735. );
  736. $serialized = serialize($to_serialize);
  737. if (is_string($serialized))
  738. {
  739. return $serialized;
  740. }
  741. else
  742. {
  743. throw new Kohana_Exception('Unable to serialize object');
  744. }
  745. }
  746. /**
  747. * JSON encoded object
  748. *
  749. * @param string json encoded object
  750. * @return bool
  751. * @throws Kohana_Exception
  752. */
  753. public function unserialize($string)
  754. {
  755. // Unserialise object
  756. $unserialized = unserialize($string);
  757. // If failed
  758. if ($unserialized === NULL)
  759. {
  760. // Throw exception
  761. throw new Kohana_Exception('Unable to correctly unserialize string: :string', array(':string' => $string));
  762. }
  763. // Foreach key/value pair
  764. foreach ($unserialized as $key => $value)
  765. {
  766. $this->$key = $value;
  767. }
  768. return TRUE;
  769. }
  770. /**
  771. * Parse the byte ranges from the HTTP_RANGE header used for
  772. * resumable downloads.
  773. *
  774. * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
  775. * @return array|FALSE
  776. */
  777. protected function _parse_byte_range()
  778. {
  779. if ( ! isset($_SERVER['HTTP_RANGE']))
  780. {
  781. return FALSE;
  782. }
  783. // TODO, speed this up with the use of string functions.
  784. preg_match_all('/(-?[0-9]++(?:-(?![0-9]++))?)(?:-?([0-9]++))?/', $_SERVER['HTTP_RANGE'], $matches, PREG_SET_ORDER);
  785. return $matches[0];
  786. }
  787. /**
  788. * Calculates the byte range to use with send_file. If HTTP_RANGE doesn't
  789. * exist then the complete byte range is returned
  790. *
  791. * @param integer $size
  792. * @return array
  793. */
  794. protected function _calculate_byte_range($size)
  795. {
  796. // Defaults to start with when the HTTP_RANGE header doesn't exist.
  797. $start = 0;
  798. $end = $size - 1;
  799. if ($range = $this->_parse_byte_range())
  800. {
  801. // We have a byte range from HTTP_RANGE
  802. $start = $range[1];
  803. if ($start[0] === '-')
  804. {
  805. // A negative value means we start from the end, so -500 would be the
  806. // last 500 bytes.
  807. $start = $size - abs($start);
  808. }
  809. if (isset($range[2]))
  810. {
  811. // Set the end range
  812. $end = $range[2];
  813. }
  814. }
  815. // Normalize values.
  816. $start = abs(intval($start));
  817. // Keep the the end value in bounds and normalize it.
  818. $end = min(abs(intval($end)), $size - 1);
  819. // Keep the start in bounds.
  820. $start = ($end < $start) ? 0 : max($start, 0);
  821. return array($start, $end);
  822. }
  823. } // End Kohana_Response