PageRenderTime 55ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/system/classes/kohana/response.php

https://bitbucket.org/thomasfortier/kohana_base
PHP | 766 lines | 396 code | 94 blank | 276 comment | 45 complexity | d180d8f612e57ae14b1c8950d9fcd786 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 {
  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. // HTTP status codes and messages
  33. public static $messages = array(
  34. // Informational 1xx
  35. 100 => 'Continue',
  36. 101 => 'Switching Protocols',
  37. // Success 2xx
  38. 200 => 'OK',
  39. 201 => 'Created',
  40. 202 => 'Accepted',
  41. 203 => 'Non-Authoritative Information',
  42. 204 => 'No Content',
  43. 205 => 'Reset Content',
  44. 206 => 'Partial Content',
  45. // Redirection 3xx
  46. 300 => 'Multiple Choices',
  47. 301 => 'Moved Permanently',
  48. 302 => 'Found', // 1.1
  49. 303 => 'See Other',
  50. 304 => 'Not Modified',
  51. 305 => 'Use Proxy',
  52. // 306 is deprecated but reserved
  53. 307 => 'Temporary Redirect',
  54. // Client Error 4xx
  55. 400 => 'Bad Request',
  56. 401 => 'Unauthorized',
  57. 402 => 'Payment Required',
  58. 403 => 'Forbidden',
  59. 404 => 'Not Found',
  60. 405 => 'Method Not Allowed',
  61. 406 => 'Not Acceptable',
  62. 407 => 'Proxy Authentication Required',
  63. 408 => 'Request Timeout',
  64. 409 => 'Conflict',
  65. 410 => 'Gone',
  66. 411 => 'Length Required',
  67. 412 => 'Precondition Failed',
  68. 413 => 'Request Entity Too Large',
  69. 414 => 'Request-URI Too Long',
  70. 415 => 'Unsupported Media Type',
  71. 416 => 'Requested Range Not Satisfiable',
  72. 417 => 'Expectation Failed',
  73. // Server Error 5xx
  74. 500 => 'Internal Server Error',
  75. 501 => 'Not Implemented',
  76. 502 => 'Bad Gateway',
  77. 503 => 'Service Unavailable',
  78. 504 => 'Gateway Timeout',
  79. 505 => 'HTTP Version Not Supported',
  80. 509 => 'Bandwidth Limit Exceeded'
  81. );
  82. /**
  83. * @var integer The response http status
  84. */
  85. protected $_status = 200;
  86. /**
  87. * @var HTTP_Header Headers returned in the response
  88. */
  89. protected $_header;
  90. /**
  91. * @var string The response body
  92. */
  93. protected $_body = '';
  94. /**
  95. * @var array Cookies to be returned in the response
  96. */
  97. protected $_cookies = array();
  98. /**
  99. * @var string The response protocol
  100. */
  101. protected $_protocol;
  102. /**
  103. * Sets up the response object
  104. *
  105. * @param array $config Setup the response object
  106. * @return void
  107. */
  108. public function __construct(array $config = array())
  109. {
  110. $this->_header = new HTTP_Header;
  111. foreach ($config as $key => $value)
  112. {
  113. if (property_exists($this, $key))
  114. {
  115. if ($key == '_header')
  116. {
  117. $this->headers($value);
  118. }
  119. else
  120. {
  121. $this->$key = $value;
  122. }
  123. }
  124. }
  125. }
  126. /**
  127. * Outputs the body when cast to string
  128. *
  129. * @return string
  130. */
  131. public function __toString()
  132. {
  133. return $this->_body;
  134. }
  135. /**
  136. * Gets or sets the body of the response
  137. *
  138. * @return mixed
  139. */
  140. public function body($content = NULL)
  141. {
  142. if ($content === NULL)
  143. return $this->_body;
  144. $this->_body = (string) $content;
  145. return $this;
  146. }
  147. /**
  148. * Gets or sets the HTTP protocol. The standard protocol to use
  149. * is `HTTP/1.1`.
  150. *
  151. * @param string $protocol Protocol to set to the request/response
  152. * @return mixed
  153. */
  154. public function protocol($protocol = NULL)
  155. {
  156. if ($protocol)
  157. {
  158. $this->_protocol = strtoupper($protocol);
  159. return $this;
  160. }
  161. if ($this->_protocol === NULL)
  162. {
  163. $this->_protocol = HTTP::$protocol;
  164. }
  165. return $this->_protocol;
  166. }
  167. /**
  168. * Sets or gets the HTTP status from this response.
  169. *
  170. * // Set the HTTP status to 404 Not Found
  171. * $response = Response::factory()
  172. * ->status(404);
  173. *
  174. * // Get the current status
  175. * $status = $response->status();
  176. *
  177. * @param integer $status Status to set to this response
  178. * @return mixed
  179. */
  180. public function status($status = NULL)
  181. {
  182. if ($status === NULL)
  183. {
  184. return $this->_status;
  185. }
  186. elseif (array_key_exists($status, Response::$messages))
  187. {
  188. $this->_status = (int) $status;
  189. return $this;
  190. }
  191. else
  192. {
  193. throw new Kohana_Exception(__METHOD__.' unknown status value : :value', array(':value' => $status));
  194. }
  195. }
  196. /**
  197. * Gets and sets headers to the [Response], allowing chaining
  198. * of response methods. If chaining isn't required, direct
  199. * access to the property should be used instead.
  200. *
  201. * // Get a header
  202. * $accept = $response->headers('Content-Type');
  203. *
  204. * // Set a header
  205. * $response->headers('Content-Type', 'text/html');
  206. *
  207. * // Get all headers
  208. * $headers = $response->headers();
  209. *
  210. * // Set multiple headers
  211. * $response->headers(array('Content-Type' => 'text/html', 'Cache-Control' => 'no-cache'));
  212. *
  213. * @param mixed $key
  214. * @param string $value
  215. * @return mixed
  216. */
  217. public function headers($key = NULL, $value = NULL)
  218. {
  219. if ($key === NULL)
  220. {
  221. return $this->_header;
  222. }
  223. elseif (is_array($key))
  224. {
  225. $this->_header->exchangeArray($key);
  226. return $this;
  227. }
  228. elseif ($value === NULL)
  229. {
  230. return Arr::get($this->_header, $key);
  231. }
  232. else
  233. {
  234. $this->_header[$key] = $value;
  235. return $this;
  236. }
  237. }
  238. /**
  239. * Returns the length of the body for use with
  240. * content header
  241. *
  242. * @return integer
  243. */
  244. public function content_length()
  245. {
  246. return strlen($this->body());
  247. }
  248. /**
  249. * Set and get cookies values for this response.
  250. *
  251. * // Get the cookies set to the response
  252. * $cookies = $response->cookie();
  253. *
  254. * // Set a cookie to the response
  255. * $response->cookie('session', array(
  256. * 'value' => $value,
  257. * 'expiration' => 12352234
  258. * ));
  259. *
  260. * @param mixed cookie name, or array of cookie values
  261. * @param string value to set to cookie
  262. * @return string
  263. * @return void
  264. * @return [Response]
  265. */
  266. public function cookie($key = NULL, $value = NULL)
  267. {
  268. // Handle the get cookie calls
  269. if ($key === NULL)
  270. return $this->_cookies;
  271. elseif ( ! is_array($key) AND ! $value)
  272. return Arr::get($this->_cookies, $key);
  273. // Handle the set cookie calls
  274. if (is_array($key))
  275. {
  276. reset($key);
  277. while (list($_key, $_value) = each($key))
  278. {
  279. $this->cookie($_key, $_value);
  280. }
  281. }
  282. else
  283. {
  284. if ( ! is_array($value))
  285. {
  286. $value = array(
  287. 'value' => $value,
  288. 'expiration' => Cookie::$expiration
  289. );
  290. }
  291. elseif ( ! isset($value['expiration']))
  292. {
  293. $value['expiration'] = Cookie::$expiration;
  294. }
  295. $this->_cookies[$key] = $value;
  296. }
  297. return $this;
  298. }
  299. /**
  300. * Deletes a cookie set to the response
  301. *
  302. * @param string name
  303. * @return Response
  304. */
  305. public function delete_cookie($name)
  306. {
  307. unset($this->_cookies[$name]);
  308. return $this;
  309. }
  310. /**
  311. * Deletes all cookies from this response
  312. *
  313. * @return Response
  314. */
  315. public function delete_cookies()
  316. {
  317. $this->_cookies = array();
  318. return $this;
  319. }
  320. /**
  321. * Sends the response status and all set headers.
  322. *
  323. * @param boolean replace existing headers
  324. * @param callback function to handle header output
  325. * @return mixed
  326. */
  327. public function send_headers($replace = FALSE, $callback = NULL)
  328. {
  329. return $this->_header->send_headers($this, $replace, $callback);
  330. }
  331. /**
  332. * Send file download as the response. All execution will be halted when
  333. * this method is called! Use TRUE for the filename to send the current
  334. * response as the file content. The third parameter allows the following
  335. * options to be set:
  336. *
  337. * Type | Option | Description | Default Value
  338. * ----------|-----------|------------------------------------|--------------
  339. * `boolean` | inline | Display inline instead of download | `FALSE`
  340. * `string` | mime_type | Manual mime type | Automatic
  341. * `boolean` | delete | Delete the file after sending | `FALSE`
  342. *
  343. * Download a file that already exists:
  344. *
  345. * $request->send_file('media/packages/kohana.zip');
  346. *
  347. * Download generated content as a file:
  348. *
  349. * $request->response($content);
  350. * $request->send_file(TRUE, $filename);
  351. *
  352. * [!!] No further processing can be done after this method is called!
  353. *
  354. * @param string filename with path, or TRUE for the current response
  355. * @param string downloaded file name
  356. * @param array additional options
  357. * @return void
  358. * @throws Kohana_Exception
  359. * @uses File::mime_by_ext
  360. * @uses File::mime
  361. * @uses Request::send_headers
  362. */
  363. public function send_file($filename, $download = NULL, array $options = NULL)
  364. {
  365. if ( ! empty($options['mime_type']))
  366. {
  367. // The mime-type has been manually set
  368. $mime = $options['mime_type'];
  369. }
  370. if ($filename === TRUE)
  371. {
  372. if (empty($download))
  373. {
  374. throw new Kohana_Exception('Download name must be provided for streaming files');
  375. }
  376. // Temporary files will automatically be deleted
  377. $options['delete'] = FALSE;
  378. if ( ! isset($mime))
  379. {
  380. // Guess the mime using the file extension
  381. $mime = File::mime_by_ext(strtolower(pathinfo($download, PATHINFO_EXTENSION)));
  382. }
  383. // Force the data to be rendered if
  384. $file_data = (string) $this->_body;
  385. // Get the content size
  386. $size = strlen($file_data);
  387. // Create a temporary file to hold the current response
  388. $file = tmpfile();
  389. // Write the current response into the file
  390. fwrite($file, $file_data);
  391. // File data is no longer needed
  392. unset($file_data);
  393. }
  394. else
  395. {
  396. // Get the complete file path
  397. $filename = realpath($filename);
  398. if (empty($download))
  399. {
  400. // Use the file name as the download file name
  401. $download = pathinfo($filename, PATHINFO_BASENAME);
  402. }
  403. // Get the file size
  404. $size = filesize($filename);
  405. if ( ! isset($mime))
  406. {
  407. // Get the mime type
  408. $mime = File::mime($filename);
  409. }
  410. // Open the file for reading
  411. $file = fopen($filename, 'rb');
  412. }
  413. if ( ! is_resource($file))
  414. {
  415. throw new Kohana_Exception('Could not read file to send: :file', array(
  416. ':file' => $download,
  417. ));
  418. }
  419. // Inline or download?
  420. $disposition = empty($options['inline']) ? 'attachment' : 'inline';
  421. // Calculate byte range to download.
  422. list($start, $end) = $this->_calculate_byte_range($size);
  423. if ( ! empty($options['resumable']))
  424. {
  425. if ($start > 0 OR $end < ($size - 1))
  426. {
  427. // Partial Content
  428. $this->_status = 206;
  429. }
  430. // Range of bytes being sent
  431. $this->_header['content-range'] = 'bytes '.$start.'-'.$end.'/'.$size;
  432. $this->_header['accept-ranges'] = 'bytes';
  433. }
  434. // Set the headers for a download
  435. $this->_header['content-disposition'] = $disposition.'; filename="'.$download.'"';
  436. $this->_header['content-type'] = $mime;
  437. $this->_header['content-length'] = (string) (($end - $start) + 1);
  438. if (Request::user_agent('browser') === 'Internet Explorer')
  439. {
  440. // Naturally, IE does not act like a real browser...
  441. if (Request::$initial->secure())
  442. {
  443. // http://support.microsoft.com/kb/316431
  444. $this->_header['pragma'] = $this->_header['cache-control'] = 'public';
  445. }
  446. if (version_compare(Request::user_agent('version'), '8.0', '>='))
  447. {
  448. // http://ajaxian.com/archives/ie-8-security
  449. $this->_header['x-content-type-options'] = 'nosniff';
  450. }
  451. }
  452. // Send all headers now
  453. $this->send_headers();
  454. while (ob_get_level())
  455. {
  456. // Flush all output buffers
  457. ob_end_flush();
  458. }
  459. // Manually stop execution
  460. ignore_user_abort(TRUE);
  461. if ( ! Kohana::$safe_mode)
  462. {
  463. // Keep the script running forever
  464. set_time_limit(0);
  465. }
  466. // Send data in 16kb blocks
  467. $block = 1024 * 16;
  468. fseek($file, $start);
  469. while ( ! feof($file) AND ($pos = ftell($file)) <= $end)
  470. {
  471. if (connection_aborted())
  472. break;
  473. if ($pos + $block > $end)
  474. {
  475. // Don't read past the buffer.
  476. $block = $end - $pos + 1;
  477. }
  478. // Output a block of the file
  479. echo fread($file, $block);
  480. // Send the data now
  481. flush();
  482. }
  483. // Close the file
  484. fclose($file);
  485. if ( ! empty($options['delete']))
  486. {
  487. try
  488. {
  489. // Attempt to remove the file
  490. unlink($filename);
  491. }
  492. catch (Exception $e)
  493. {
  494. // Create a text version of the exception
  495. $error = Kohana_Exception::text($e);
  496. if (is_object(Kohana::$log))
  497. {
  498. // Add this exception to the log
  499. Kohana::$log->add(Log::ERROR, $error);
  500. // Make sure the logs are written
  501. Kohana::$log->write();
  502. }
  503. // Do NOT display the exception, it will corrupt the output!
  504. }
  505. }
  506. // Stop execution
  507. exit;
  508. }
  509. /**
  510. * Renders the HTTP_Interaction to a string, producing
  511. *
  512. * - Protocol
  513. * - Headers
  514. * - Body
  515. *
  516. * @return string
  517. */
  518. public function render()
  519. {
  520. if ( ! $this->_header->offsetExists('content-type'))
  521. {
  522. // Add the default Content-Type header if required
  523. $this->_header['content-type'] = Kohana::$content_type.'; charset='.Kohana::$charset;
  524. }
  525. // Set the content length
  526. $this->headers('content-length', (string) $this->content_length());
  527. // If Kohana expose, set the user-agent
  528. if (Kohana::$expose)
  529. {
  530. $this->headers('user-agent', 'Kohana Framework '.Kohana::VERSION.' ('.Kohana::CODENAME.')');
  531. }
  532. // Prepare cookies
  533. if ($this->_cookies)
  534. {
  535. if (extension_loaded('http'))
  536. {
  537. $this->_header['set-cookie'] = http_build_cookie($this->_cookies);
  538. }
  539. else
  540. {
  541. $cookies = array();
  542. // Parse each
  543. foreach ($this->_cookies as $key => $value)
  544. {
  545. $string = $key.'='.$value['value'].'; expires='.date('l, d M Y H:i:s T', $value['expiration']);
  546. $cookies[] = $string;
  547. }
  548. // Create the cookie string
  549. $this->_header['set-cookie'] = $cookies;
  550. }
  551. }
  552. $output = $this->_protocol.' '.$this->_status.' '.Response::$messages[$this->_status]."\r\n";
  553. $output .= (string) $this->_header;
  554. $output .= $this->_body;
  555. return $output;
  556. }
  557. /**
  558. * Generate ETag
  559. * Generates an ETag from the response ready to be returned
  560. *
  561. * @throws Request_Exception
  562. * @return String Generated ETag
  563. */
  564. public function generate_etag()
  565. {
  566. if ($this->_body === NULL)
  567. {
  568. throw new Request_Exception('No response yet associated with request - cannot auto generate resource ETag');
  569. }
  570. // Generate a unique hash for the response
  571. return '"'.sha1($this->render()).'"';
  572. }
  573. /**
  574. * Check Cache
  575. * Checks the browser cache to see the response needs to be returned
  576. *
  577. * @param string $etag Resource ETag
  578. * @param Request $request The request to test against
  579. * @return Response
  580. * @throws Request_Exception
  581. */
  582. public function check_cache($etag = NULL, Request $request = NULL)
  583. {
  584. if ( ! $etag)
  585. {
  586. $etag = $this->generate_etag();
  587. }
  588. if ( ! $request)
  589. throw new Request_Exception('A Request object must be supplied with an etag for evaluation');
  590. // Set the ETag header
  591. $this->_header['etag'] = $etag;
  592. // Add the Cache-Control header if it is not already set
  593. // This allows etags to be used with max-age, etc
  594. if ($this->_header->offsetExists('cache-control'))
  595. {
  596. if (is_array($this->_header['cache-control']))
  597. {
  598. $this->_header['cache-control'][] = new HTTP_Header_Value('must-revalidate');
  599. }
  600. else
  601. {
  602. $this->_header['cache-control'] = $this->_header['cache-control'].', must-revalidate';
  603. }
  604. }
  605. else
  606. {
  607. $this->_header['cache-control'] = 'must-revalidate';
  608. }
  609. if ($request->headers('if-none-match') AND (string) $request->headers('if-none-match') === $etag)
  610. {
  611. // No need to send data again
  612. $this->_status = 304;
  613. $this->send_headers();
  614. // Stop execution
  615. exit;
  616. }
  617. return $this;
  618. }
  619. /**
  620. * Parse the byte ranges from the HTTP_RANGE header used for
  621. * resumable downloads.
  622. *
  623. * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
  624. * @return array|FALSE
  625. */
  626. protected function _parse_byte_range()
  627. {
  628. if ( ! isset($_SERVER['HTTP_RANGE']))
  629. {
  630. return FALSE;
  631. }
  632. // TODO, speed this up with the use of string functions.
  633. preg_match_all('/(-?[0-9]++(?:-(?![0-9]++))?)(?:-?([0-9]++))?/', $_SERVER['HTTP_RANGE'], $matches, PREG_SET_ORDER);
  634. return $matches[0];
  635. }
  636. /**
  637. * Calculates the byte range to use with send_file. If HTTP_RANGE doesn't
  638. * exist then the complete byte range is returned
  639. *
  640. * @param integer $size
  641. * @return array
  642. */
  643. protected function _calculate_byte_range($size)
  644. {
  645. // Defaults to start with when the HTTP_RANGE header doesn't exist.
  646. $start = 0;
  647. $end = $size - 1;
  648. if ($range = $this->_parse_byte_range())
  649. {
  650. // We have a byte range from HTTP_RANGE
  651. $start = $range[1];
  652. if ($start[0] === '-')
  653. {
  654. // A negative value means we start from the end, so -500 would be the
  655. // last 500 bytes.
  656. $start = $size - abs($start);
  657. }
  658. if (isset($range[2]))
  659. {
  660. // Set the end range
  661. $end = $range[2];
  662. }
  663. }
  664. // Normalize values.
  665. $start = abs(intval($start));
  666. // Keep the the end value in bounds and normalize it.
  667. $end = min(abs(intval($end)), $size - 1);
  668. // Keep the start in bounds.
  669. $start = ($end < $start) ? 0 : max($start, 0);
  670. return array($start, $end);
  671. }
  672. } // End Kohana_Response