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

/system/classes/kohana/request.php

https://bitbucket.org/alvinpd/monsterninja
PHP | 1134 lines | 620 code | 138 blank | 376 comment | 57 complexity | 07c326e83ccced822b13fe8656244204 MD5 | raw file
  1. <?php defined('SYSPATH') or die('No direct script access.');
  2. /**
  3. * Request and response wrapper. Uses the [Route] class to determine what
  4. * [Controller] to send the request to.
  5. *
  6. * @package Kohana
  7. * @category Base
  8. * @author Kohana Team
  9. * @copyright (c) 2008-2009 Kohana Team
  10. * @license http://kohanaphp.com/license
  11. */
  12. class Kohana_Request {
  13. // HTTP status codes and messages
  14. public static $messages = array(
  15. // Informational 1xx
  16. 100 => 'Continue',
  17. 101 => 'Switching Protocols',
  18. // Success 2xx
  19. 200 => 'OK',
  20. 201 => 'Created',
  21. 202 => 'Accepted',
  22. 203 => 'Non-Authoritative Information',
  23. 204 => 'No Content',
  24. 205 => 'Reset Content',
  25. 206 => 'Partial Content',
  26. 207 => 'Multi-Status',
  27. // Redirection 3xx
  28. 300 => 'Multiple Choices',
  29. 301 => 'Moved Permanently',
  30. 302 => 'Found', // 1.1
  31. 303 => 'See Other',
  32. 304 => 'Not Modified',
  33. 305 => 'Use Proxy',
  34. // 306 is deprecated but reserved
  35. 307 => 'Temporary Redirect',
  36. // Client Error 4xx
  37. 400 => 'Bad Request',
  38. 401 => 'Unauthorized',
  39. 402 => 'Payment Required',
  40. 403 => 'Forbidden',
  41. 404 => 'Not Found',
  42. 405 => 'Method Not Allowed',
  43. 406 => 'Not Acceptable',
  44. 407 => 'Proxy Authentication Required',
  45. 408 => 'Request Timeout',
  46. 409 => 'Conflict',
  47. 410 => 'Gone',
  48. 411 => 'Length Required',
  49. 412 => 'Precondition Failed',
  50. 413 => 'Request Entity Too Large',
  51. 414 => 'Request-URI Too Long',
  52. 415 => 'Unsupported Media Type',
  53. 416 => 'Requested Range Not Satisfiable',
  54. 417 => 'Expectation Failed',
  55. 422 => 'Unprocessable Entity',
  56. 423 => 'Locked',
  57. 424 => 'Failed Dependency',
  58. // Server Error 5xx
  59. 500 => 'Internal Server Error',
  60. 501 => 'Not Implemented',
  61. 502 => 'Bad Gateway',
  62. 503 => 'Service Unavailable',
  63. 504 => 'Gateway Timeout',
  64. 505 => 'HTTP Version Not Supported',
  65. 507 => 'Insufficient Storage',
  66. 509 => 'Bandwidth Limit Exceeded'
  67. );
  68. /**
  69. * @var string method: GET, POST, PUT, DELETE, etc
  70. */
  71. public static $method = 'GET';
  72. /**
  73. * @var string protocol: http, https, ftp, cli, etc
  74. */
  75. public static $protocol = 'http';
  76. /**
  77. * @var string referring URL
  78. */
  79. public static $referrer;
  80. /**
  81. * @var string client user agent
  82. */
  83. public static $user_agent = '';
  84. /**
  85. * @var string client IP address
  86. */
  87. public static $client_ip = '0.0.0.0';
  88. /**
  89. * @var boolean AJAX-generated request
  90. */
  91. public static $is_ajax = FALSE;
  92. /**
  93. * @var object main request instance
  94. */
  95. public static $instance;
  96. /**
  97. * @var object currently executing request instance
  98. */
  99. public static $current;
  100. /**
  101. * Main request singleton instance. If no URI is provided, the URI will
  102. * be automatically detected using PATH_INFO, REQUEST_URI, or PHP_SELF.
  103. *
  104. * $request = Request::instance();
  105. *
  106. * @param string URI of the request
  107. * @return Request
  108. */
  109. public static function instance( & $uri = TRUE)
  110. {
  111. if ( ! Request::$instance)
  112. {
  113. if (Kohana::$is_cli)
  114. {
  115. // Default protocol for command line is cli://
  116. Request::$protocol = 'cli';
  117. // Get the command line options
  118. $options = CLI::options('uri', 'method', 'get', 'post');
  119. if (isset($options['uri']))
  120. {
  121. // Use the specified URI
  122. $uri = $options['uri'];
  123. }
  124. if (isset($options['method']))
  125. {
  126. // Use the specified method
  127. Request::$method = strtoupper($options['method']);
  128. }
  129. if (isset($options['get']))
  130. {
  131. // Overload the global GET data
  132. parse_str($options['get'], $_GET);
  133. }
  134. if (isset($options['post']))
  135. {
  136. // Overload the global POST data
  137. parse_str($options['post'], $_POST);
  138. }
  139. }
  140. else
  141. {
  142. if (isset($_SERVER['REQUEST_METHOD']))
  143. {
  144. // Use the server request method
  145. Request::$method = $_SERVER['REQUEST_METHOD'];
  146. }
  147. if ( ! empty($_SERVER['HTTPS']) AND filter_var($_SERVER['HTTPS'], FILTER_VALIDATE_BOOLEAN))
  148. {
  149. // This request is secure
  150. Request::$protocol = 'https';
  151. }
  152. if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) AND strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest')
  153. {
  154. // This request is an AJAX request
  155. Request::$is_ajax = TRUE;
  156. }
  157. if (isset($_SERVER['HTTP_REFERER']))
  158. {
  159. // There is a referrer for this request
  160. Request::$referrer = $_SERVER['HTTP_REFERER'];
  161. }
  162. if (isset($_SERVER['HTTP_USER_AGENT']))
  163. {
  164. // Set the client user agent
  165. Request::$user_agent = $_SERVER['HTTP_USER_AGENT'];
  166. }
  167. if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
  168. {
  169. // Use the forwarded IP address, typically set when the
  170. // client is using a proxy server.
  171. Request::$client_ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
  172. }
  173. elseif (isset($_SERVER['HTTP_CLIENT_IP']))
  174. {
  175. // Use the forwarded IP address, typically set when the
  176. // client is using a proxy server.
  177. Request::$client_ip = $_SERVER['HTTP_CLIENT_IP'];
  178. }
  179. elseif (isset($_SERVER['REMOTE_ADDR']))
  180. {
  181. // The remote IP address
  182. Request::$client_ip = $_SERVER['REMOTE_ADDR'];
  183. }
  184. if (Request::$method !== 'GET' AND Request::$method !== 'POST')
  185. {
  186. // Methods besides GET and POST do not properly parse the form-encoded
  187. // query string into the $_POST array, so we overload it manually.
  188. parse_str(file_get_contents('php://input'), $_POST);
  189. }
  190. if ($uri === TRUE)
  191. {
  192. if ( ! empty($_SERVER['PATH_INFO']))
  193. {
  194. // PATH_INFO does not contain the docroot or index
  195. $uri = $_SERVER['PATH_INFO'];
  196. }
  197. else
  198. {
  199. // REQUEST_URI and PHP_SELF include the docroot and index
  200. if (isset($_SERVER['REQUEST_URI']))
  201. {
  202. // REQUEST_URI includes the query string, remove it
  203. $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
  204. }
  205. elseif (isset($_SERVER['PHP_SELF']))
  206. {
  207. $uri = $_SERVER['PHP_SELF'];
  208. }
  209. elseif (isset($_SERVER['REDIRECT_URL']))
  210. {
  211. $uri = $_SERVER['REDIRECT_URL'];
  212. }
  213. else
  214. {
  215. // If you ever see this error, please report an issue at and include a dump of $_SERVER
  216. // http://dev.kohanaphp.com/projects/kohana3/issues
  217. throw new Kohana_Exception('Unable to detect the URI using PATH_INFO, REQUEST_URI, or PHP_SELF');
  218. }
  219. // Get the path from the base URL, including the index file
  220. $base_url = parse_url(Kohana::$base_url, PHP_URL_PATH);
  221. if (strpos($uri, $base_url) === 0)
  222. {
  223. // Remove the base URL from the URI
  224. $uri = substr($uri, strlen($base_url));
  225. }
  226. if (Kohana::$index_file AND strpos($uri, Kohana::$index_file) === 0)
  227. {
  228. // Remove the index file from the URI
  229. $uri = substr($uri, strlen(Kohana::$index_file));
  230. }
  231. }
  232. }
  233. }
  234. // Reduce multiple slashes to a single slash
  235. $uri = preg_replace('#//+#', '/', $uri);
  236. // Remove all dot-paths from the URI, they are not valid
  237. $uri = preg_replace('#\.[\s./]*/#', '', $uri);
  238. // Create the instance singleton
  239. Request::$instance = Request::$current = new Request($uri);
  240. // Add the default Content-Type header
  241. Request::$instance->headers['Content-Type'] = 'text/html; charset='.Kohana::$charset;
  242. }
  243. return Request::$instance;
  244. }
  245. /**
  246. * Return the currently executing request. This is changed to the current
  247. * request when [Request::execute] is called and restored when the request
  248. * is completed.
  249. *
  250. * $request = Request::current();
  251. *
  252. * @return Request
  253. * @since 3.0.5
  254. */
  255. public static function current()
  256. {
  257. return Request::$current;
  258. }
  259. /**
  260. * Creates a new request object for the given URI. This differs from
  261. * [Request::instance] in that it does not automatically detect the URI
  262. * and should only be used for creating HMVC requests.
  263. *
  264. * $request = Request::factory($uri);
  265. *
  266. * @param string URI of the request
  267. * @return Request
  268. */
  269. public static function factory($uri)
  270. {
  271. return new Request($uri);
  272. }
  273. /**
  274. * Returns information about the client user agent.
  275. *
  276. * // Returns "Chrome" when using Google Chrome
  277. * $browser = Request::user_agent('browser');
  278. *
  279. * @param string value to return: browser, version, robot, mobile, platform
  280. * @return string requested information
  281. * @return FALSE no information found
  282. * @uses Kohana::config
  283. * @uses Request::$user_agent
  284. */
  285. public static function user_agent($value)
  286. {
  287. static $info;
  288. if (isset($info[$value]))
  289. {
  290. // This value has already been found
  291. return $info[$value];
  292. }
  293. if ($value === 'browser' OR $value == 'version')
  294. {
  295. // Load browsers
  296. $browsers = Kohana::config('user_agents')->browser;
  297. foreach ($browsers as $search => $name)
  298. {
  299. if (stripos(Request::$user_agent, $search) !== FALSE)
  300. {
  301. // Set the browser name
  302. $info['browser'] = $name;
  303. if (preg_match('#'.preg_quote($search).'[^0-9.]*+([0-9.][0-9.a-z]*)#i', Request::$user_agent, $matches))
  304. {
  305. // Set the version number
  306. $info['version'] = $matches[1];
  307. }
  308. else
  309. {
  310. // No version number found
  311. $info['version'] = FALSE;
  312. }
  313. return $info[$value];
  314. }
  315. }
  316. }
  317. else
  318. {
  319. // Load the search group for this type
  320. $group = Kohana::config('user_agents')->$value;
  321. foreach ($group as $search => $name)
  322. {
  323. if (stripos(Request::$user_agent, $search) !== FALSE)
  324. {
  325. // Set the value name
  326. return $info[$value] = $name;
  327. }
  328. }
  329. }
  330. // The value requested could not be found
  331. return $info[$value] = FALSE;
  332. }
  333. /**
  334. * Returns the accepted content types. If a specific type is defined,
  335. * the quality of that type will be returned.
  336. *
  337. * $types = Request::accept_type();
  338. *
  339. * @param string content MIME type
  340. * @return float when checking a specific type
  341. * @return array
  342. * @uses Request::_parse_accept
  343. */
  344. public static function accept_type($type = NULL)
  345. {
  346. static $accepts;
  347. if ($accepts === NULL)
  348. {
  349. // Parse the HTTP_ACCEPT header
  350. $accepts = Request::_parse_accept($_SERVER['HTTP_ACCEPT'], array('*/*' => 1.0));
  351. }
  352. if (isset($type))
  353. {
  354. // Return the quality setting for this type
  355. return isset($accepts[$type]) ? $accepts[$type] : $accepts['*/*'];
  356. }
  357. return $accepts;
  358. }
  359. /**
  360. * Returns the accepted languages. If a specific language is defined,
  361. * the quality of that language will be returned. If the language is not
  362. * accepted, FALSE will be returned.
  363. *
  364. * $langs = Request::accept_lang();
  365. *
  366. * @param string language code
  367. * @return float when checking a specific language
  368. * @return array
  369. * @uses Request::_parse_accept
  370. */
  371. public static function accept_lang($lang = NULL)
  372. {
  373. static $accepts;
  374. if ($accepts === NULL)
  375. {
  376. // Parse the HTTP_ACCEPT_LANGUAGE header
  377. $accepts = Request::_parse_accept($_SERVER['HTTP_ACCEPT_LANGUAGE']);
  378. }
  379. if (isset($lang))
  380. {
  381. // Return the quality setting for this lang
  382. return isset($accepts[$lang]) ? $accepts[$lang] : FALSE;
  383. }
  384. return $accepts;
  385. }
  386. /**
  387. * Returns the accepted encodings. If a specific encoding is defined,
  388. * the quality of that encoding will be returned. If the encoding is not
  389. * accepted, FALSE will be returned.
  390. *
  391. * $encodings = Request::accept_encoding();
  392. *
  393. * @param string encoding type
  394. * @return float when checking a specific encoding
  395. * @return array
  396. * @uses Request::_parse_accept
  397. */
  398. public static function accept_encoding($type = NULL)
  399. {
  400. static $accepts;
  401. if ($accepts === NULL)
  402. {
  403. // Parse the HTTP_ACCEPT_LANGUAGE header
  404. $accepts = Request::_parse_accept($_SERVER['HTTP_ACCEPT_ENCODING']);
  405. }
  406. if (isset($type))
  407. {
  408. // Return the quality setting for this type
  409. return isset($accepts[$type]) ? $accepts[$type] : FALSE;
  410. }
  411. return $accepts;
  412. }
  413. /**
  414. * Parses an accept header and returns an array (type => quality) of the
  415. * accepted types, ordered by quality.
  416. *
  417. * $accept = Request::_parse_accept($header, $defaults);
  418. *
  419. * @param string header to parse
  420. * @param array default values
  421. * @return array
  422. */
  423. protected static function _parse_accept( & $header, array $accepts = NULL)
  424. {
  425. if ( ! empty($header))
  426. {
  427. // Get all of the types
  428. $types = explode(',', $header);
  429. foreach ($types as $type)
  430. {
  431. // Split the type into parts
  432. $parts = explode(';', $type);
  433. // Make the type only the MIME
  434. $type = trim(array_shift($parts));
  435. // Default quality is 1.0
  436. $quality = 1.0;
  437. foreach ($parts as $part)
  438. {
  439. // Prevent undefined $value notice below
  440. if (strpos($part, '=') === FALSE)
  441. continue;
  442. // Separate the key and value
  443. list ($key, $value) = explode('=', trim($part));
  444. if ($key === 'q')
  445. {
  446. // There is a quality for this type
  447. $quality = (float) trim($value);
  448. }
  449. }
  450. // Add the accept type and quality
  451. $accepts[$type] = $quality;
  452. }
  453. }
  454. // Make sure that accepts is an array
  455. $accepts = (array) $accepts;
  456. // Order by quality
  457. arsort($accepts);
  458. return $accepts;
  459. }
  460. /**
  461. * @var object route matched for this request
  462. */
  463. public $route;
  464. /**
  465. * @var integer HTTP response code: 200, 404, 500, etc
  466. */
  467. public $status = 200;
  468. /**
  469. * @var string response body
  470. */
  471. public $response = '';
  472. /**
  473. * @var array headers to send with the response body
  474. */
  475. public $headers = array();
  476. /**
  477. * @var string controller directory
  478. */
  479. public $directory = '';
  480. /**
  481. * @var string controller to be executed
  482. */
  483. public $controller;
  484. /**
  485. * @var string action to be executed in the controller
  486. */
  487. public $action;
  488. /**
  489. * @var string the URI of the request
  490. */
  491. public $uri;
  492. // Parameters extracted from the route
  493. protected $_params;
  494. /**
  495. * Creates a new request object for the given URI. New requests should be
  496. * created using the [Request::instance] or [Request::factory] methods.
  497. *
  498. * $request = new Request($uri);
  499. *
  500. * @param string URI of the request
  501. * @return void
  502. * @throws Kohana_Request_Exception
  503. * @uses Route::all
  504. * @uses Route::matches
  505. */
  506. public function __construct($uri)
  507. {
  508. // Remove trailing slashes from the URI
  509. $uri = trim($uri, '/');
  510. // Load routes
  511. $routes = Route::all();
  512. foreach ($routes as $name => $route)
  513. {
  514. if ($params = $route->matches($uri))
  515. {
  516. // Store the URI
  517. $this->uri = $uri;
  518. // Store the matching route
  519. $this->route = $route;
  520. if (isset($params['directory']))
  521. {
  522. // Controllers are in a sub-directory
  523. $this->directory = $params['directory'];
  524. }
  525. // Store the controller
  526. $this->controller = $params['controller'];
  527. if (isset($params['action']))
  528. {
  529. // Store the action
  530. $this->action = $params['action'];
  531. }
  532. else
  533. {
  534. // Use the default action
  535. $this->action = Route::$default_action;
  536. }
  537. // These are accessible as public vars and can be overloaded
  538. unset($params['controller'], $params['action'], $params['directory']);
  539. // Params cannot be changed once matched
  540. $this->_params = $params;
  541. return;
  542. }
  543. }
  544. // No matching route for this URI
  545. $this->status = 404;
  546. throw new Kohana_Request_Exception('Unable to find a route to match the URI: :uri',
  547. array(':uri' => $uri));
  548. }
  549. /**
  550. * Returns the response as the string representation of a request.
  551. *
  552. * echo $request;
  553. *
  554. * @return string
  555. */
  556. public function __toString()
  557. {
  558. return (string) $this->response;
  559. }
  560. /**
  561. * Generates a relative URI for the current route.
  562. *
  563. * $request->uri($params);
  564. *
  565. * @param array additional route parameters
  566. * @return string
  567. * @uses Route::uri
  568. */
  569. public function uri(array $params = NULL)
  570. {
  571. if ( ! isset($params['directory']))
  572. {
  573. // Add the current directory
  574. $params['directory'] = $this->directory;
  575. }
  576. if ( ! isset($params['controller']))
  577. {
  578. // Add the current controller
  579. $params['controller'] = $this->controller;
  580. }
  581. if ( ! isset($params['action']))
  582. {
  583. // Add the current action
  584. $params['action'] = $this->action;
  585. }
  586. // Add the current parameters
  587. $params += $this->_params;
  588. return $this->route->uri($params);
  589. }
  590. /**
  591. * Retrieves a value from the route parameters.
  592. *
  593. * $id = $request->param('id');
  594. *
  595. * @param string key of the value
  596. * @param mixed default value if the key is not set
  597. * @return mixed
  598. */
  599. public function param($key = NULL, $default = NULL)
  600. {
  601. if ($key === NULL)
  602. {
  603. // Return the full array
  604. return $this->_params;
  605. }
  606. return isset($this->_params[$key]) ? $this->_params[$key] : $default;
  607. }
  608. /**
  609. * Sends the response status and all set headers. The current server
  610. * protocol (HTTP/1.0 or HTTP/1.1) will be used when available. If not
  611. * available, HTTP/1.1 will be used.
  612. *
  613. * $request->send_headers();
  614. *
  615. * @return $this
  616. * @uses Request::$messages
  617. */
  618. public function send_headers()
  619. {
  620. if ( ! headers_sent())
  621. {
  622. if (isset($_SERVER['SERVER_PROTOCOL']))
  623. {
  624. // Use the default server protocol
  625. $protocol = $_SERVER['SERVER_PROTOCOL'];
  626. }
  627. else
  628. {
  629. // Default to using newer protocol
  630. $protocol = 'HTTP/1.1';
  631. }
  632. // HTTP status line
  633. header($protocol.' '.$this->status.' '.Request::$messages[$this->status]);
  634. foreach ($this->headers as $name => $value)
  635. {
  636. if (is_string($name))
  637. {
  638. // Combine the name and value to make a raw header
  639. $value = "{$name}: {$value}";
  640. }
  641. // Send the raw header
  642. header($value, TRUE);
  643. }
  644. }
  645. return $this;
  646. }
  647. /**
  648. * Redirects as the request response. If the URL does not include a
  649. * protocol, it will be converted into a complete URL.
  650. *
  651. * $request->redirect($url);
  652. *
  653. * [!!] No further processing can be done after this method is called!
  654. *
  655. * @param string redirect location
  656. * @param integer status code: 301, 302, etc
  657. * @return void
  658. * @uses URL::site
  659. * @uses Request::send_headers
  660. */
  661. public function redirect($url, $code = 302)
  662. {
  663. if (strpos($url, '://') === FALSE)
  664. {
  665. // Make the URI into a URL
  666. $url = URL::site($url, TRUE);
  667. }
  668. // Set the response status
  669. $this->status = $code;
  670. // Set the location header
  671. $this->headers['Location'] = $url;
  672. // Send headers
  673. $this->send_headers();
  674. // Stop execution
  675. exit;
  676. }
  677. /**
  678. * Send file download as the response. All execution will be halted when
  679. * this method is called! Use TRUE for the filename to send the current
  680. * response as the file content. The third parameter allows the following
  681. * options to be set:
  682. *
  683. * Type | Option | Description | Default Value
  684. * ----------|-----------|------------------------------------|--------------
  685. * `boolean` | inline | Display inline instead of download | `FALSE`
  686. * `string` | mime_type | Manual mime type | Automatic
  687. * `boolean` | delete | Delete the file after sending | `FALSE`
  688. *
  689. * Download a file that already exists:
  690. *
  691. * $request->send_file('media/packages/kohana.zip');
  692. *
  693. * Download generated content as a file:
  694. *
  695. * $request->response = $content;
  696. * $request->send_file(TRUE, $filename);
  697. *
  698. * [!!] No further processing can be done after this method is called!
  699. *
  700. * @param string filename with path, or TRUE for the current response
  701. * @param string downloaded file name
  702. * @param array additional options
  703. * @return void
  704. * @throws Kohana_Exception
  705. * @uses File::mime_by_ext
  706. * @uses File::mime
  707. * @uses Request::send_headers
  708. */
  709. public function send_file($filename, $download = NULL, array $options = NULL)
  710. {
  711. if ( ! empty($options['mime_type']))
  712. {
  713. // The mime-type has been manually set
  714. $mime = $options['mime_type'];
  715. }
  716. if ($filename === TRUE)
  717. {
  718. if (empty($download))
  719. {
  720. throw new Kohana_Exception('Download name must be provided for streaming files');
  721. }
  722. // Temporary files will automatically be deleted
  723. $options['delete'] = FALSE;
  724. if ( ! isset($mime))
  725. {
  726. // Guess the mime using the file extension
  727. $mime = File::mime_by_ext(strtolower(pathinfo($download, PATHINFO_EXTENSION)));
  728. }
  729. // Get the content size
  730. $size = strlen($this->response);
  731. // Create a temporary file to hold the current response
  732. $file = tmpfile();
  733. // Write the current response into the file
  734. fwrite($file, $this->response);
  735. // Prepare the file for reading
  736. fseek($file, 0);
  737. }
  738. else
  739. {
  740. // Get the complete file path
  741. $filename = realpath($filename);
  742. if (empty($download))
  743. {
  744. // Use the file name as the download file name
  745. $download = pathinfo($filename, PATHINFO_BASENAME);
  746. }
  747. // Get the file size
  748. $size = filesize($filename);
  749. if ( ! isset($mime))
  750. {
  751. // Get the mime type
  752. $mime = File::mime($filename);
  753. }
  754. // Open the file for reading
  755. $file = fopen($filename, 'rb');
  756. }
  757. // Inline or download?
  758. $disposition = empty($options['inline']) ? 'attachment' : 'inline';
  759. // Set the headers for a download
  760. $this->headers['Content-Disposition'] = $disposition.'; filename="'.$download.'"';
  761. $this->headers['Content-Type'] = $mime;
  762. $this->headers['Content-Length'] = $size;
  763. if ( ! empty($options['resumable']))
  764. {
  765. // @todo: ranged download processing
  766. }
  767. // Send all headers now
  768. $this->send_headers();
  769. while (ob_get_level())
  770. {
  771. // Flush all output buffers
  772. ob_end_flush();
  773. }
  774. // Manually stop execution
  775. ignore_user_abort(TRUE);
  776. // Keep the script running forever
  777. set_time_limit(0);
  778. // Send data in 16kb blocks
  779. $block = 1024 * 16;
  780. while ( ! feof($file))
  781. {
  782. if (connection_aborted())
  783. break;
  784. // Output a block of the file
  785. echo fread($file, $block);
  786. // Send the data now
  787. flush();
  788. }
  789. // Close the file
  790. fclose($file);
  791. if ( ! empty($options['delete']))
  792. {
  793. try
  794. {
  795. // Attempt to remove the file
  796. unlink($filename);
  797. }
  798. catch (Exception $e)
  799. {
  800. // Create a text version of the exception
  801. $error = Kohana::exception_text($e);
  802. if (is_object(Kohana::$log))
  803. {
  804. // Add this exception to the log
  805. Kohana::$log->add(Kohana::ERROR, $error);
  806. // Make sure the logs are written
  807. Kohana::$log->write();
  808. }
  809. // Do NOT display the exception, it will corrupt the output!
  810. }
  811. }
  812. // Stop execution
  813. exit;
  814. }
  815. /**
  816. * Processes the request, executing the controller action that handles this
  817. * request, determined by the [Route].
  818. *
  819. * 1. Before the controller action is called, the [Controller::before] method
  820. * will be called.
  821. * 2. Next the controller action will be called.
  822. * 3. After the controller action is called, the [Controller::after] method
  823. * will be called.
  824. *
  825. * By default, the output from the controller is captured and returned, and
  826. * no headers are sent.
  827. *
  828. * $request->execute();
  829. *
  830. * @return $this
  831. * @throws Kohana_Exception
  832. * @uses [Kohana::$profiling]
  833. * @uses [Profiler]
  834. */
  835. public function execute()
  836. {
  837. // Create the class prefix
  838. $prefix = 'controller_';
  839. if ($this->directory)
  840. {
  841. // Add the directory name to the class prefix
  842. $prefix .= str_replace(array('\\', '/'), '_', trim($this->directory, '/')).'_';
  843. }
  844. if (Kohana::$profiling)
  845. {
  846. // Set the benchmark name
  847. $benchmark = '"'.$this->uri.'"';
  848. if ($this !== Request::$instance AND Request::$current)
  849. {
  850. // Add the parent request uri
  851. $benchmark .= ' « "'.Request::$current->uri.'"';
  852. }
  853. // Start benchmarking
  854. $benchmark = Profiler::start('Requests', $benchmark);
  855. }
  856. // Store the currently active request
  857. $previous = Request::$current;
  858. // Change the current request to this request
  859. Request::$current = $this;
  860. try
  861. {
  862. // Load the controller using reflection
  863. $class = new ReflectionClass($prefix.$this->controller);
  864. if ($class->isAbstract())
  865. {
  866. throw new Kohana_Exception('Cannot create instances of abstract :controller',
  867. array(':controller' => $prefix.$this->controller));
  868. }
  869. // Create a new instance of the controller
  870. $controller = $class->newInstance($this);
  871. // Execute the "before action" method
  872. $class->getMethod('before')->invoke($controller);
  873. // Determine the action to use
  874. $action = empty($this->action) ? Route::$default_action : $this->action;
  875. // Execute the main action with the parameters
  876. $class->getMethod('action_'.$action)->invokeArgs($controller, $this->_params);
  877. // Execute the "after action" method
  878. $class->getMethod('after')->invoke($controller);
  879. }
  880. catch (Exception $e)
  881. {
  882. // Restore the previous request
  883. Request::$current = $previous;
  884. if (isset($benchmark))
  885. {
  886. // Delete the benchmark, it is invalid
  887. Profiler::delete($benchmark);
  888. }
  889. if ($e instanceof ReflectionException)
  890. {
  891. // Reflection will throw exceptions for missing classes or actions
  892. $this->status = 404;
  893. }
  894. else
  895. {
  896. // All other exceptions are PHP/server errors
  897. $this->status = 500;
  898. }
  899. // Re-throw the exception
  900. throw $e;
  901. }
  902. // Restore the previous request
  903. Request::$current = $previous;
  904. if (isset($benchmark))
  905. {
  906. // Stop the benchmark
  907. Profiler::stop($benchmark);
  908. }
  909. return $this;
  910. }
  911. /**
  912. * Generates an [ETag](http://en.wikipedia.org/wiki/HTTP_ETag) from the
  913. * request response.
  914. *
  915. * $etag = $request->generate_etag();
  916. *
  917. * [!!] If the request response is empty when this method is called, an
  918. * exception will be thrown!
  919. *
  920. * @return string
  921. * @throws Kohana_Request_Exception
  922. */
  923. public function generate_etag()
  924. {
  925. if ($this->response === NULL)
  926. {
  927. throw new Kohana_Request_Exception('No response yet associated with request - cannot auto generate resource ETag');
  928. }
  929. // Generate a unique hash for the response
  930. return '"'.sha1($this->response).'"';
  931. }
  932. /**
  933. * Checks the browser cache to see the response needs to be returned.
  934. *
  935. * $request->check_cache($etag);
  936. *
  937. * [!!] If the cache check succeeds, no further processing can be done!
  938. *
  939. * @param string etag to check
  940. * @return $this
  941. * @throws Kohana_Request_Exception
  942. * @uses Request::generate_etag
  943. */
  944. public function check_cache($etag = null)
  945. {
  946. if (empty($etag))
  947. {
  948. $etag = $this->generate_etag();
  949. }
  950. // Set the ETag header
  951. $this->headers['ETag'] = $etag;
  952. // Add the Cache-Control header if it is not already set
  953. // This allows etags to be used with Max-Age, etc
  954. $this->headers += array(
  955. 'Cache-Control' => 'must-revalidate',
  956. );
  957. if (isset($_SERVER['HTTP_IF_NONE_MATCH']) AND $_SERVER['HTTP_IF_NONE_MATCH'] === $etag)
  958. {
  959. // No need to send data again
  960. $this->status = 304;
  961. $this->send_headers();
  962. // Stop execution
  963. exit;
  964. }
  965. return $this;
  966. }
  967. } // End Request