PageRenderTime 54ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/system/classes/kohana/request.php

https://bitbucket.org/emolina/asesores
PHP | 1326 lines | 710 code | 165 blank | 451 comment | 70 complexity | 5af16ac425773d6f183ed6deb33e4a21 MD5 | raw file
Possible License(s): BSD-3-Clause
  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. /**
  232. * We use REQUEST_URI as the fallback value. The reason
  233. * for this is we might have a malformed URL such as:
  234. *
  235. * http://localhost/http://example.com/judge.php
  236. *
  237. * which parse_url can't handle. So rather than leave empty
  238. * handed, we'll use this.
  239. */
  240. $uri = $_SERVER['REQUEST_URI'];
  241. if($request_uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH))
  242. {
  243. // Valid URL path found, set it.
  244. $uri = $request_uri;
  245. }
  246. // Decode the request URI
  247. $uri = rawurldecode($uri);
  248. }
  249. elseif (isset($_SERVER['PHP_SELF']))
  250. {
  251. $uri = $_SERVER['PHP_SELF'];
  252. }
  253. elseif (isset($_SERVER['REDIRECT_URL']))
  254. {
  255. $uri = $_SERVER['REDIRECT_URL'];
  256. }
  257. else
  258. {
  259. // If you ever see this error, please report an issue at http://dev.kohanaphp.com/projects/kohana3/issues
  260. // along with any relevant information about your web server setup. Thanks!
  261. throw new Kohana_Exception('Unable to detect the URI using PATH_INFO, REQUEST_URI, PHP_SELF or REDIRECT_URL');
  262. }
  263. // Get the path from the base URL, including the index file
  264. $base_url = parse_url(Kohana::$base_url, PHP_URL_PATH);
  265. if (strpos($uri, $base_url) === 0)
  266. {
  267. // Remove the base URL from the URI
  268. $uri = (string) substr($uri, strlen($base_url));
  269. }
  270. if (Kohana::$index_file AND strpos($uri, Kohana::$index_file) === 0)
  271. {
  272. // Remove the index file from the URI
  273. $uri = (string) substr($uri, strlen(Kohana::$index_file));
  274. }
  275. }
  276. return $uri;
  277. }
  278. /**
  279. * Return the currently executing request. This is changed to the current
  280. * request when [Request::execute] is called and restored when the request
  281. * is completed.
  282. *
  283. * $request = Request::current();
  284. *
  285. * @return Request
  286. * @since 3.0.5
  287. */
  288. public static function current()
  289. {
  290. return Request::$current;
  291. }
  292. /**
  293. * Creates a new request object for the given URI. This differs from
  294. * [Request::instance] in that it does not automatically detect the URI
  295. * and should only be used for creating HMVC requests.
  296. *
  297. * $request = Request::factory($uri);
  298. *
  299. * @param string URI of the request
  300. * @return Request
  301. */
  302. public static function factory($uri)
  303. {
  304. return new Request($uri);
  305. }
  306. /**
  307. * Returns information about the client user agent.
  308. *
  309. * // Returns "Chrome" when using Google Chrome
  310. * $browser = Request::user_agent('browser');
  311. *
  312. * Multiple values can be returned at once by using an array:
  313. *
  314. * // Get the browser and platform with a single call
  315. * $info = Request::user_agent(array('browser', 'platform'));
  316. *
  317. * When using an array for the value, an associative array will be returned.
  318. *
  319. * @param mixed string to return: browser, version, robot, mobile, platform; or array of values
  320. * @return mixed requested information, FALSE if nothing is found
  321. * @uses Kohana::config
  322. * @uses Request::$user_agent
  323. */
  324. public static function user_agent($value)
  325. {
  326. if (is_array($value))
  327. {
  328. $agent = array();
  329. foreach ($value as $v)
  330. {
  331. // Add each key to the set
  332. $agent[$v] = Request::user_agent($v);
  333. }
  334. return $agent;
  335. }
  336. static $info;
  337. if (isset($info[$value]))
  338. {
  339. // This value has already been found
  340. return $info[$value];
  341. }
  342. if ($value === 'browser' OR $value == 'version')
  343. {
  344. // Load browsers
  345. $browsers = Kohana::config('user_agents')->browser;
  346. foreach ($browsers as $search => $name)
  347. {
  348. if (stripos(Request::$user_agent, $search) !== FALSE)
  349. {
  350. // Set the browser name
  351. $info['browser'] = $name;
  352. if (preg_match('#'.preg_quote($search).'[^0-9.]*+([0-9.][0-9.a-z]*)#i', Request::$user_agent, $matches))
  353. {
  354. // Set the version number
  355. $info['version'] = $matches[1];
  356. }
  357. else
  358. {
  359. // No version number found
  360. $info['version'] = FALSE;
  361. }
  362. return $info[$value];
  363. }
  364. }
  365. }
  366. else
  367. {
  368. // Load the search group for this type
  369. $group = Kohana::config('user_agents')->$value;
  370. foreach ($group as $search => $name)
  371. {
  372. if (stripos(Request::$user_agent, $search) !== FALSE)
  373. {
  374. // Set the value name
  375. return $info[$value] = $name;
  376. }
  377. }
  378. }
  379. // The value requested could not be found
  380. return $info[$value] = FALSE;
  381. }
  382. /**
  383. * Returns the accepted content types. If a specific type is defined,
  384. * the quality of that type will be returned.
  385. *
  386. * $types = Request::accept_type();
  387. *
  388. * @param string content MIME type
  389. * @return float when checking a specific type
  390. * @return array
  391. * @uses Request::_parse_accept
  392. */
  393. public static function accept_type($type = NULL)
  394. {
  395. static $accepts;
  396. if ($accepts === NULL)
  397. {
  398. // Parse the HTTP_ACCEPT header
  399. $accepts = Request::_parse_accept($_SERVER['HTTP_ACCEPT'], array('*/*' => 1.0));
  400. }
  401. if (isset($type))
  402. {
  403. // Return the quality setting for this type
  404. return isset($accepts[$type]) ? $accepts[$type] : $accepts['*/*'];
  405. }
  406. return $accepts;
  407. }
  408. /**
  409. * Returns the accepted languages. If a specific language is defined,
  410. * the quality of that language will be returned. If the language is not
  411. * accepted, FALSE will be returned.
  412. *
  413. * $langs = Request::accept_lang();
  414. *
  415. * @param string language code
  416. * @return float when checking a specific language
  417. * @return array
  418. * @uses Request::_parse_accept
  419. */
  420. public static function accept_lang($lang = NULL)
  421. {
  422. static $accepts;
  423. if ($accepts === NULL)
  424. {
  425. // Parse the HTTP_ACCEPT_LANGUAGE header
  426. $accepts = Request::_parse_accept($_SERVER['HTTP_ACCEPT_LANGUAGE']);
  427. }
  428. if (isset($lang))
  429. {
  430. // Return the quality setting for this lang
  431. return isset($accepts[$lang]) ? $accepts[$lang] : FALSE;
  432. }
  433. return $accepts;
  434. }
  435. /**
  436. * Returns the accepted encodings. If a specific encoding is defined,
  437. * the quality of that encoding will be returned. If the encoding is not
  438. * accepted, FALSE will be returned.
  439. *
  440. * $encodings = Request::accept_encoding();
  441. *
  442. * @param string encoding type
  443. * @return float when checking a specific encoding
  444. * @return array
  445. * @uses Request::_parse_accept
  446. */
  447. public static function accept_encoding($type = NULL)
  448. {
  449. static $accepts;
  450. if ($accepts === NULL)
  451. {
  452. // Parse the HTTP_ACCEPT_LANGUAGE header
  453. $accepts = Request::_parse_accept($_SERVER['HTTP_ACCEPT_ENCODING']);
  454. }
  455. if (isset($type))
  456. {
  457. // Return the quality setting for this type
  458. return isset($accepts[$type]) ? $accepts[$type] : FALSE;
  459. }
  460. return $accepts;
  461. }
  462. /**
  463. * Parses an accept header and returns an array (type => quality) of the
  464. * accepted types, ordered by quality.
  465. *
  466. * $accept = Request::_parse_accept($header, $defaults);
  467. *
  468. * @param string header to parse
  469. * @param array default values
  470. * @return array
  471. */
  472. protected static function _parse_accept( & $header, array $accepts = NULL)
  473. {
  474. if ( ! empty($header))
  475. {
  476. // Get all of the types
  477. $types = explode(',', $header);
  478. foreach ($types as $type)
  479. {
  480. // Split the type into parts
  481. $parts = explode(';', $type);
  482. // Make the type only the MIME
  483. $type = trim(array_shift($parts));
  484. // Default quality is 1.0
  485. $quality = 1.0;
  486. foreach ($parts as $part)
  487. {
  488. // Prevent undefined $value notice below
  489. if (strpos($part, '=') === FALSE)
  490. continue;
  491. // Separate the key and value
  492. list ($key, $value) = explode('=', trim($part));
  493. if ($key === 'q')
  494. {
  495. // There is a quality for this type
  496. $quality = (float) trim($value);
  497. }
  498. }
  499. // Add the accept type and quality
  500. $accepts[$type] = $quality;
  501. }
  502. }
  503. // Make sure that accepts is an array
  504. $accepts = (array) $accepts;
  505. // Order by quality
  506. arsort($accepts);
  507. return $accepts;
  508. }
  509. /**
  510. * @var object route matched for this request
  511. */
  512. public $route;
  513. /**
  514. * @var integer HTTP response code: 200, 404, 500, etc
  515. */
  516. public $status = 200;
  517. /**
  518. * @var string response body
  519. */
  520. public $response = '';
  521. /**
  522. * @var array headers to send with the response body
  523. */
  524. public $headers = array();
  525. /**
  526. * @var string controller directory
  527. */
  528. public $directory = '';
  529. /**
  530. * @var string controller to be executed
  531. */
  532. public $controller;
  533. /**
  534. * @var string action to be executed in the controller
  535. */
  536. public $action;
  537. /**
  538. * @var string the URI of the request
  539. */
  540. public $uri;
  541. // Parameters extracted from the route
  542. protected $_params;
  543. /**
  544. * Creates a new request object for the given URI. New requests should be
  545. * created using the [Request::instance] or [Request::factory] methods.
  546. *
  547. * $request = new Request($uri);
  548. *
  549. * @param string URI of the request
  550. * @return void
  551. * @throws Kohana_Request_Exception
  552. * @uses Route::all
  553. * @uses Route::matches
  554. */
  555. public function __construct($uri)
  556. {
  557. // Remove trailing slashes from the URI
  558. $uri = trim($uri, '/');
  559. // Load routes
  560. $routes = Route::all();
  561. foreach ($routes as $name => $route)
  562. {
  563. if ($params = $route->matches($uri))
  564. {
  565. // Store the URI
  566. $this->uri = $uri;
  567. // Store the matching route
  568. $this->route = $route;
  569. if (isset($params['directory']))
  570. {
  571. // Controllers are in a sub-directory
  572. $this->directory = $params['directory'];
  573. }
  574. // Store the controller
  575. $this->controller = $params['controller'];
  576. if (isset($params['action']))
  577. {
  578. // Store the action
  579. $this->action = $params['action'];
  580. }
  581. else
  582. {
  583. // Use the default action
  584. $this->action = Route::$default_action;
  585. }
  586. // These are accessible as public vars and can be overloaded
  587. unset($params['controller'], $params['action'], $params['directory']);
  588. // Params cannot be changed once matched
  589. $this->_params = $params;
  590. return;
  591. }
  592. }
  593. // No matching route for this URI
  594. $this->status = 404;
  595. throw new Kohana_Request_Exception('Unable to find a route to match the URI: :uri',
  596. array(':uri' => $uri));
  597. }
  598. /**
  599. * Returns the response as the string representation of a request.
  600. *
  601. * echo $request;
  602. *
  603. * @return string
  604. */
  605. public function __toString()
  606. {
  607. return (string) $this->response;
  608. }
  609. /**
  610. * Generates a relative URI for the current route.
  611. *
  612. * $request->uri($params);
  613. *
  614. * @param array additional route parameters
  615. * @return string
  616. * @uses Route::uri
  617. */
  618. public function uri(array $params = NULL)
  619. {
  620. if ( ! isset($params['directory']))
  621. {
  622. // Add the current directory
  623. $params['directory'] = $this->directory;
  624. }
  625. if ( ! isset($params['controller']))
  626. {
  627. // Add the current controller
  628. $params['controller'] = $this->controller;
  629. }
  630. if ( ! isset($params['action']))
  631. {
  632. // Add the current action
  633. $params['action'] = $this->action;
  634. }
  635. // Add the current parameters
  636. $params += $this->_params;
  637. return $this->route->uri($params);
  638. }
  639. /**
  640. * Create a URL from the current request. This is a shortcut for:
  641. *
  642. * echo URL::site($this->request->uri($params), $protocol);
  643. *
  644. * @param string route name
  645. * @param array URI parameters
  646. * @param mixed protocol string or boolean, adds protocol and domain
  647. * @return string
  648. * @since 3.0.7
  649. * @uses URL::site
  650. */
  651. public function url(array $params = NULL, $protocol = NULL)
  652. {
  653. // Create a URI with the current route and convert it to a URL
  654. return URL::site($this->uri($params), $protocol);
  655. }
  656. /**
  657. * Retrieves a value from the route parameters.
  658. *
  659. * $id = $request->param('id');
  660. *
  661. * @param string key of the value
  662. * @param mixed default value if the key is not set
  663. * @return mixed
  664. */
  665. public function param($key = NULL, $default = NULL)
  666. {
  667. if ($key === NULL)
  668. {
  669. // Return the full array
  670. return $this->_params;
  671. }
  672. return isset($this->_params[$key]) ? $this->_params[$key] : $default;
  673. }
  674. /**
  675. * Sends the response status and all set headers. The current server
  676. * protocol (HTTP/1.0 or HTTP/1.1) will be used when available. If not
  677. * available, HTTP/1.1 will be used.
  678. *
  679. * $request->send_headers();
  680. *
  681. * @return $this
  682. * @uses Request::$messages
  683. */
  684. public function send_headers()
  685. {
  686. if ( ! headers_sent())
  687. {
  688. if (isset($_SERVER['SERVER_PROTOCOL']))
  689. {
  690. // Use the default server protocol
  691. $protocol = $_SERVER['SERVER_PROTOCOL'];
  692. }
  693. else
  694. {
  695. // Default to using newer protocol
  696. $protocol = 'HTTP/1.1';
  697. }
  698. // HTTP status line
  699. header($protocol.' '.$this->status.' '.Request::$messages[$this->status]);
  700. foreach ($this->headers as $name => $value)
  701. {
  702. if (is_string($name))
  703. {
  704. // Combine the name and value to make a raw header
  705. $value = "{$name}: {$value}";
  706. }
  707. // Send the raw header
  708. header($value, TRUE);
  709. }
  710. foreach (Session::$instances as $session)
  711. {
  712. // Sessions will most likely write cookies, which will be sent
  713. // with the headers
  714. $session->write();
  715. }
  716. }
  717. return $this;
  718. }
  719. /**
  720. * Redirects as the request response. If the URL does not include a
  721. * protocol, it will be converted into a complete URL.
  722. *
  723. * $request->redirect($url);
  724. *
  725. * [!!] No further processing can be done after this method is called!
  726. *
  727. * @param string redirect location
  728. * @param integer status code: 301, 302, etc
  729. * @return void
  730. * @uses URL::site
  731. * @uses Request::send_headers
  732. */
  733. public function redirect($url = '', $code = 302)
  734. {
  735. if (strpos($url, '://') === FALSE)
  736. {
  737. // Make the URI into a URL
  738. $url = URL::site($url, TRUE);
  739. }
  740. // Set the response status
  741. $this->status = $code;
  742. // Set the location header
  743. $this->headers['Location'] = $url;
  744. // Send headers
  745. $this->send_headers();
  746. // Stop execution
  747. exit;
  748. }
  749. /**
  750. * Send file download as the response. All execution will be halted when
  751. * this method is called! Use TRUE for the filename to send the current
  752. * response as the file content. The third parameter allows the following
  753. * options to be set:
  754. *
  755. * Type | Option | Description | Default Value
  756. * ----------|-----------|------------------------------------|--------------
  757. * `boolean` | inline | Display inline instead of download | `FALSE`
  758. * `string` | mime_type | Manual mime type | Automatic
  759. * `boolean` | delete | Delete the file after sending | `FALSE`
  760. *
  761. * Download a file that already exists:
  762. *
  763. * $request->send_file('media/packages/kohana.zip');
  764. *
  765. * Download generated content as a file:
  766. *
  767. * $request->response = $content;
  768. * $request->send_file(TRUE, $filename);
  769. *
  770. * [!!] No further processing can be done after this method is called!
  771. *
  772. * @param string filename with path, or TRUE for the current response
  773. * @param string downloaded file name
  774. * @param array additional options
  775. * @return void
  776. * @throws Kohana_Exception
  777. * @uses File::mime_by_ext
  778. * @uses File::mime
  779. * @uses Request::send_headers
  780. */
  781. public function send_file($filename, $download = NULL, array $options = NULL)
  782. {
  783. if ( ! empty($options['mime_type']))
  784. {
  785. // The mime-type has been manually set
  786. $mime = $options['mime_type'];
  787. }
  788. if ($filename === TRUE)
  789. {
  790. if (empty($download))
  791. {
  792. throw new Kohana_Exception('Download name must be provided for streaming files');
  793. }
  794. // Temporary files will automatically be deleted
  795. $options['delete'] = FALSE;
  796. if ( ! isset($mime))
  797. {
  798. // Guess the mime using the file extension
  799. $mime = File::mime_by_ext(strtolower(pathinfo($download, PATHINFO_EXTENSION)));
  800. }
  801. // Force the data to be rendered if
  802. $file_data = (string) $this->response;
  803. // Get the content size
  804. $size = strlen($file_data);
  805. // Create a temporary file to hold the current response
  806. $file = tmpfile();
  807. // Write the current response into the file
  808. fwrite($file, $file_data);
  809. // File data is no longer needed
  810. unset($file_data);
  811. }
  812. else
  813. {
  814. // Get the complete file path
  815. $filename = realpath($filename);
  816. if (empty($download))
  817. {
  818. // Use the file name as the download file name
  819. $download = pathinfo($filename, PATHINFO_BASENAME);
  820. }
  821. // Get the file size
  822. $size = filesize($filename);
  823. if ( ! isset($mime))
  824. {
  825. // Get the mime type
  826. $mime = File::mime($filename);
  827. }
  828. // Open the file for reading
  829. $file = fopen($filename, 'rb');
  830. }
  831. if ( ! is_resource($file))
  832. {
  833. throw new Kohana_Exception('Could not read file to send: :file', array(
  834. ':file' => $download,
  835. ));
  836. }
  837. // Inline or download?
  838. $disposition = empty($options['inline']) ? 'attachment' : 'inline';
  839. // Calculate byte range to download.
  840. list($start, $end) = $this->_calculate_byte_range($size);
  841. if ( ! empty($options['resumable']))
  842. {
  843. if ($start > 0 OR $end < ($size - 1))
  844. {
  845. // Partial Content
  846. $this->status = 206;
  847. }
  848. // Range of bytes being sent
  849. $this->headers['Content-Range'] = 'bytes '.$start.'-'.$end.'/'.$size;
  850. $this->headers['Accept-Ranges'] = 'bytes';
  851. }
  852. // Set the headers for a download
  853. $this->headers['Content-Disposition'] = $disposition.'; filename="'.$download.'"';
  854. $this->headers['Content-Type'] = $mime;
  855. $this->headers['Content-Length'] = ($end - $start) + 1;
  856. if (Request::user_agent('browser') === 'Internet Explorer')
  857. {
  858. // Naturally, IE does not act like a real browser...
  859. if (Request::$protocol === 'https')
  860. {
  861. // http://support.microsoft.com/kb/316431
  862. $this->headers['Pragma'] = $this->headers['Cache-Control'] = 'public';
  863. }
  864. if (version_compare(Request::user_agent('version'), '8.0', '>='))
  865. {
  866. // http://ajaxian.com/archives/ie-8-security
  867. $this->headers['X-Content-Type-Options'] = 'nosniff';
  868. }
  869. }
  870. // Send all headers now
  871. $this->send_headers();
  872. while (ob_get_level())
  873. {
  874. // Flush all output buffers
  875. ob_end_flush();
  876. }
  877. // Manually stop execution
  878. ignore_user_abort(TRUE);
  879. if ( ! Kohana::$safe_mode)
  880. {
  881. // Keep the script running forever
  882. set_time_limit(0);
  883. }
  884. // Send data in 16kb blocks
  885. $block = 1024 * 16;
  886. fseek($file, $start);
  887. while ( ! feof($file) AND ($pos = ftell($file)) <= $end)
  888. {
  889. if (connection_aborted())
  890. break;
  891. if ($pos + $block > $end)
  892. {
  893. // Don't read past the buffer.
  894. $block = $end - $pos + 1;
  895. }
  896. // Output a block of the file
  897. echo fread($file, $block);
  898. // Send the data now
  899. flush();
  900. }
  901. // Close the file
  902. fclose($file);
  903. if ( ! empty($options['delete']))
  904. {
  905. try
  906. {
  907. // Attempt to remove the file
  908. unlink($filename);
  909. }
  910. catch (Exception $e)
  911. {
  912. // Create a text version of the exception
  913. $error = Kohana::exception_text($e);
  914. if (is_object(Kohana::$log))
  915. {
  916. // Add this exception to the log
  917. Kohana::$log->add(Kohana::ERROR, $error);
  918. // Make sure the logs are written
  919. Kohana::$log->write();
  920. }
  921. // Do NOT display the exception, it will corrupt the output!
  922. }
  923. }
  924. // Stop execution
  925. exit;
  926. }
  927. /**
  928. * Parse the byte ranges from the HTTP_RANGE header used for
  929. * resumable downloads.
  930. *
  931. * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
  932. * @return array|FALSE
  933. */
  934. protected function _parse_byte_range()
  935. {
  936. if ( ! isset($_SERVER['HTTP_RANGE']))
  937. {
  938. return FALSE;
  939. }
  940. // TODO, speed this up with the use of string functions.
  941. preg_match_all('/(-?[0-9]++(?:-(?![0-9]++))?)(?:-?([0-9]++))?/', $_SERVER['HTTP_RANGE'], $matches, PREG_SET_ORDER);
  942. return $matches[0];
  943. }
  944. /**
  945. * Calculates the byte range to use with send_file. If HTTP_RANGE doesn't
  946. * exist then the complete byte range is returned
  947. *
  948. * @param integer $size
  949. * @return array
  950. */
  951. protected function _calculate_byte_range($size)
  952. {
  953. // Defaults to start with when the HTTP_RANGE header doesn't exist.
  954. $start = 0;
  955. $end = $size - 1;
  956. if ($range = $this->_parse_byte_range())
  957. {
  958. // We have a byte range from HTTP_RANGE
  959. $start = $range[1];
  960. if ($start[0] === '-')
  961. {
  962. // A negative value means we start from the end, so -500 would be the
  963. // last 500 bytes.
  964. $start = $size - abs($start);
  965. }
  966. if (isset($range[2]))
  967. {
  968. // Set the end range
  969. $end = $range[2];
  970. }
  971. }
  972. // Normalize values.
  973. $start = abs(intval($start));
  974. // Keep the the end value in bounds and normalize it.
  975. $end = min(abs(intval($end)), $size - 1);
  976. // Keep the start in bounds.
  977. $start = ($end < $start) ? 0 : max($start, 0);
  978. return array($start, $end);
  979. }
  980. /**
  981. * Processes the request, executing the controller action that handles this
  982. * request, determined by the [Route].
  983. *
  984. * 1. Before the controller action is called, the [Controller::before] method
  985. * will be called.
  986. * 2. Next the controller action will be called.
  987. * 3. After the controller action is called, the [Controller::after] method
  988. * will be called.
  989. *
  990. * By default, the output from the controller is captured and returned, and
  991. * no headers are sent.
  992. *
  993. * $request->execute();
  994. *
  995. * @return $this
  996. * @throws Kohana_Exception
  997. * @uses [Kohana::$profiling]
  998. * @uses [Profiler]
  999. */
  1000. public function execute()
  1001. {
  1002. // Create the class prefix
  1003. $prefix = 'controller_';
  1004. if ($this->directory)
  1005. {
  1006. // Add the directory name to the class prefix
  1007. $prefix .= str_replace(array('\\', '/'), '_', trim($this->directory, '/')).'_';
  1008. }
  1009. if (Kohana::$profiling)
  1010. {
  1011. // Set the benchmark name
  1012. $benchmark = '"'.$this->uri.'"';
  1013. if ($this !== Request::$instance AND Request::$current)
  1014. {
  1015. // Add the parent request uri
  1016. $benchmark .= ' ÂŤ "'.Request::$current->uri.'"';
  1017. }
  1018. // Start benchmarking
  1019. $benchmark = Profiler::start('Requests', $benchmark);
  1020. }
  1021. // Store the currently active request
  1022. $previous = Request::$current;
  1023. // Change the current request to this request
  1024. Request::$current = $this;
  1025. try
  1026. {
  1027. // Load the controller using reflection
  1028. $class = new ReflectionClass($prefix.$this->controller);
  1029. if ($class->isAbstract())
  1030. {
  1031. throw new Kohana_Exception('Cannot create instances of abstract :controller',
  1032. array(':controller' => $prefix.$this->controller));
  1033. }
  1034. // Create a new instance of the controller
  1035. $controller = $class->newInstance($this);
  1036. // Execute the "before action" method
  1037. $class->getMethod('before')->invoke($controller);
  1038. // Determine the action to use
  1039. $action = empty($this->action) ? Route::$default_action : $this->action;
  1040. // Execute the main action with the parameters
  1041. $class->getMethod('action_'.$action)->invokeArgs($controller, $this->_params);
  1042. // Execute the "after action" method
  1043. $class->getMethod('after')->invoke($controller);
  1044. }
  1045. catch (Exception $e)
  1046. {
  1047. // Restore the previous request
  1048. Request::$current = $previous;
  1049. if (isset($benchmark))
  1050. {
  1051. // Delete the benchmark, it is invalid
  1052. Profiler::delete($benchmark);
  1053. }
  1054. if ($e instanceof ReflectionException)
  1055. {
  1056. // Reflection will throw exceptions for missing classes or actions
  1057. $this->status = 404;
  1058. }
  1059. else
  1060. {
  1061. // All other exceptions are PHP/server errors
  1062. $this->status = 500;
  1063. }
  1064. // Re-throw the exception
  1065. throw $e;
  1066. }
  1067. // Restore the previous request
  1068. Request::$current = $previous;
  1069. if (isset($benchmark))
  1070. {
  1071. // Stop the benchmark
  1072. Profiler::stop($benchmark);
  1073. }
  1074. return $this;
  1075. }
  1076. /**
  1077. * Generates an [ETag](http://en.wikipedia.org/wiki/HTTP_ETag) from the
  1078. * request response.
  1079. *
  1080. * $etag = $request->generate_etag();
  1081. *
  1082. * [!!] If the request response is empty when this method is called, an
  1083. * exception will be thrown!
  1084. *
  1085. * @return string
  1086. * @throws Kohana_Request_Exception
  1087. */
  1088. public function generate_etag()
  1089. {
  1090. if ($this->response === NULL)
  1091. {
  1092. throw new Kohana_Request_Exception('No response yet associated with request - cannot auto generate resource ETag');
  1093. }
  1094. // Generate a unique hash for the response
  1095. return '"'.sha1($this->response).'"';
  1096. }
  1097. /**
  1098. * Checks the browser cache to see the response needs to be returned.
  1099. *
  1100. * $request->check_cache($etag);
  1101. *
  1102. * [!!] If the cache check succeeds, no further processing can be done!
  1103. *
  1104. * @param string etag to check
  1105. * @return $this
  1106. * @throws Kohana_Request_Exception
  1107. * @uses Request::generate_etag
  1108. */
  1109. public function check_cache($etag = null)
  1110. {
  1111. if (empty($etag))
  1112. {
  1113. $etag = $this->generate_etag();
  1114. }
  1115. // Set the ETag header
  1116. $this->headers['ETag'] = $etag;
  1117. // Add the Cache-Control header if it is not already set
  1118. // This allows etags to be used with Max-Age, etc
  1119. $this->headers += array(
  1120. 'Cache-Control' => 'must-revalidate',
  1121. );
  1122. if (isset($_SERVER['HTTP_IF_NONE_MATCH']) AND $_SERVER['HTTP_IF_NONE_MATCH'] === $etag)
  1123. {
  1124. // No need to send data again
  1125. $this->status = 304;
  1126. $this->send_headers();
  1127. // Stop execution
  1128. exit;
  1129. }
  1130. return $this;
  1131. }
  1132. } // End Request