PageRenderTime 247ms CodeModel.GetById 100ms app.highlight 19ms RepoModel.GetById 122ms app.codeStats 0ms

/system/classes/Kohana/Response.php

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