PageRenderTime 53ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/system/classes/kohana/request.php

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