Response.php 10.9 KB
Newer Older
Qiang Xue committed
1 2 3
<?php
/**
 * @link http://www.yiiframework.com/
Qiang Xue committed
4
 * @copyright Copyright (c) 2008 Yii Software LLC
Qiang Xue committed
5 6 7 8 9
 * @license http://www.yiiframework.com/license/
 */

namespace yii\web;

Qiang Xue committed
10
use Yii;
11
use yii\base\HttpException;
Qiang Xue committed
12
use yii\helpers\FileHelper;
Qiang Xue committed
13
use yii\helpers\Html;
14
use yii\helpers\StringHelper;
Qiang Xue committed
15 16 17 18 19 20 21

/**
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class Response extends \yii\base\Response
{
Qiang Xue committed
22 23 24 25 26 27 28 29
	/**
	 * @var integer the HTTP status code that should be used when redirecting in AJAX mode.
	 * This is used by [[redirect()]]. A 2xx code should normally be used for this purpose
	 * so that the AJAX handler will treat the response as a success.
	 * @see redirect
	 */
	public $ajaxRedirectCode = 278;

Qiang Xue committed
30 31 32 33 34 35
	/**
	 * Sends a file to user.
	 * @param string $fileName file name
	 * @param string $content content to be set.
	 * @param string $mimeType mime type of the content. If null, it will be guessed automatically based on the given file name.
	 * @param boolean $terminate whether to terminate the current application after calling this method
36
	 * @throws \yii\base\HttpException when range request is not satisfiable.
Qiang Xue committed
37 38 39
	 */
	public function sendFile($fileName, $content, $mimeType = null, $terminate = true)
	{
40 41
		if ($mimeType === null && (($mimeType = FileHelper::getMimeTypeByExtension($fileName)) === null)) {
			$mimeType = 'application/octet-stream';
Qiang Xue committed
42
		}
43

44
		$fileSize = StringHelper::strlen($content);
45 46 47
		$contentStart = 0;
		$contentEnd = $fileSize - 1;

48 49
		// tell the client that we accept range requests
		header('Accept-Ranges: bytes');
50

51 52
		if (isset($_SERVER['HTTP_RANGE'])) {
			// client sent us a multibyte range, can not hold this one for now
53
			if (strpos(',', $_SERVER['HTTP_RANGE']) !== false) {
54
				header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
55
				throw new HttpException(416, 'Requested Range Not Satisfiable');
56 57
			}

58
			$range = str_replace('bytes=', '', $_SERVER['HTTP_RANGE']);
59

60
			// range requests starts from "-", so it means that data must be dumped the end point.
61
			if ($range[0] === '-') {
62
				$contentStart = $fileSize - substr($range, 1);
63
			} else {
64
				$range = explode('-', $range);
65
				$contentStart = $range[0];
66
				$contentEnd = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $fileSize - 1;
67 68 69 70 71 72 73 74 75 76 77 78 79
			}

			/* Check the range and make sure it's treated according to the specs.
			 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
			 */
			// End bytes can not be larger than $end.
			$contentEnd = ($contentEnd > $fileSize) ? $fileSize : $contentEnd;

			// Validate the requested range and return an error if it's not correct.
			$wrongContentStart = ($contentStart > $contentEnd || $contentStart > $fileSize - 1 || $contentStart < 0);

			if ($wrongContentStart) {   
				header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
80
				throw new HttpException(416, 'Requested Range Not Satisfiable');
81 82 83 84 85 86 87 88 89 90
			}

			header('HTTP/1.1 206 Partial Content');
			header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
		} else {
			header('HTTP/1.1 200 OK');
		}

		$length = $contentEnd - $contentStart + 1; // Calculate new content length

Qiang Xue committed
91 92 93
		header('Pragma: public');
		header('Expires: 0');
		header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
94 95 96
		header('Content-Type: ' . $mimeType);
		header('Content-Length: ' . $length);
		header('Content-Disposition: attachment; filename="' . $fileName . '"');
Qiang Xue committed
97
		header('Content-Transfer-Encoding: binary');
98
		$content = StringHelper::substr($content, $contentStart, $length);
Qiang Xue committed
99 100 101 102

		if ($terminate) {
			// clean up the application first because the file downloading could take long time
			// which may cause timeout of some resources (such as DB connection)
103
			ob_start();
104
			Yii::$app->end(0, false);
105
			ob_end_clean();
Qiang Xue committed
106 107
			echo $content;
			exit(0);
Ragazzo committed
108
		} else {
109
			echo $content;
Ragazzo committed
110
		}
Qiang Xue committed
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
	}

	/**
	 * Sends existing file to a browser as a download using x-sendfile.
	 *
	 * X-Sendfile is a feature allowing a web application to redirect the request for a file to the webserver
	 * that in turn processes the request, this way eliminating the need to perform tasks like reading the file
	 * and sending it to the user. When dealing with a lot of files (or very big files) this can lead to a great
	 * increase in performance as the web application is allowed to terminate earlier while the webserver is
	 * handling the request.
	 *
	 * The request is sent to the server through a special non-standard HTTP-header.
	 * When the web server encounters the presence of such header it will discard all output and send the file
	 * specified by that header using web server internals including all optimizations like caching-headers.
	 *
	 * As this header directive is non-standard different directives exists for different web servers applications:
	 * <ul>
	 * <li>Apache: {@link http://tn123.org/mod_xsendfile X-Sendfile}</li>
	 * <li>Lighttpd v1.4: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-LIGHTTPD-send-file}</li>
	 * <li>Lighttpd v1.5: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-Sendfile}</li>
	 * <li>Nginx: {@link http://wiki.nginx.org/XSendfile X-Accel-Redirect}</li>
	 * <li>Cherokee: {@link http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile X-Sendfile and X-Accel-Redirect}</li>
	 * </ul>
	 * So for this method to work the X-SENDFILE option/module should be enabled by the web server and
	 * a proper xHeader should be sent.
	 *
	 * <b>Note:</b>
	 * This option allows to download files that are not under web folders, and even files that are otherwise protected (deny from all) like .htaccess
	 *
	 * <b>Side effects</b>:
	 * If this option is disabled by the web server, when this method is called a download configuration dialog
	 * will open but the downloaded file will have 0 bytes.
	 *
Qiang Xue committed
144 145 146 147 148
	 * <b>Known issues</b>:
	 * There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show
	 * an error message like this: "Internet Explorer was not able to open this Internet site. The requested site is either unavailable or cannot be found.".
	 * You can work around this problem by removing the <code>Pragma</code>-header.
	 *
Qiang Xue committed
149 150 151
	 * <b>Example</b>:
	 * <pre>
	 * <?php
resurtm committed
152 153 154 155
	 *    Yii::app()->request->xSendFile('/home/user/Pictures/picture1.jpg', array(
	 *        'saveName' => 'image1.jpg',
	 *        'mimeType' => 'image/jpeg',
	 *        'terminate' => false,
Qiang Xue committed
156 157 158 159 160 161 162 163 164 165
	 *    ));
	 * ?>
	 * </pre>
	 * @param string $filePath file name with full path
	 * @param array $options additional options:
	 * <ul>
	 * <li>saveName: file name shown to the user, if not set real file name will be used</li>
	 * <li>mimeType: mime type of the file, if not set it will be guessed automatically based on the file name, if set to null no content-type header will be sent.</li>
	 * <li>xHeader: appropriate x-sendfile header, defaults to "X-Sendfile"</li>
	 * <li>terminate: whether to terminate the current application after calling this method, defaults to true</li>
166 167
	 * <li>forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true</li>
	 * <li>addHeaders: an array of additional http headers in header-value pairs</li>
Qiang Xue committed
168
	 * </ul>
169
	 * @todo
Qiang Xue committed
170
	 */
Qiang Xue committed
171
	public function xSendFile($filePath, $options = array())
Qiang Xue committed
172
	{
Qiang Xue committed
173 174 175 176 177
		if (!isset($options['forceDownload']) || $options['forceDownload']) {
			$disposition = 'attachment';
		} else {
			$disposition = 'inline';
		}
Qiang Xue committed
178

Qiang Xue committed
179 180 181
		if (!isset($options['saveName'])) {
			$options['saveName'] = basename($filePath);
		}
Qiang Xue committed
182

Qiang Xue committed
183 184 185 186
		if (!isset($options['mimeType'])) {
			if (($options['mimeType'] = CFileHelper::getMimeTypeByExtension($filePath)) === null) {
				$options['mimeType'] = 'text/plain';
			}
Qiang Xue committed
187 188
		}

Qiang Xue committed
189 190 191
		if (!isset($options['xHeader'])) {
			$options['xHeader'] = 'X-Sendfile';
		}
Qiang Xue committed
192

Qiang Xue committed
193 194 195 196 197 198 199 200
		if ($options['mimeType'] !== null) {
			header('Content-type: ' . $options['mimeType']);
		}
		header('Content-Disposition: ' . $disposition . '; filename="' . $options['saveName'] . '"');
		if (isset($options['addHeaders'])) {
			foreach ($options['addHeaders'] as $header => $value) {
				header($header . ': ' . $value);
			}
Qiang Xue committed
201
		}
Qiang Xue committed
202
		header(trim($options['xHeader']) . ': ' . $filePath);
Qiang Xue committed
203

Qiang Xue committed
204 205 206
		if (!isset($options['terminate']) || $options['terminate']) {
			Yii::$app->end();
		}
Qiang Xue committed
207
	}
Qiang Xue committed
208 209 210

	/**
	 * Redirects the browser to the specified URL.
Qiang Xue committed
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
	 * This method will send out a "Location" header to achieve the redirection.
	 * In AJAX mode, this normally will not work as expected unless there are some
	 * client-side JavaScript code handling the redirection. To help achieve this goal,
	 * this method will use [[ajaxRedirectCode]] as the HTTP status code when performing
	 * redirection in AJAX mode. The following JavaScript code may be used on the client
	 * side to handle the redirection response:
	 *
	 * ~~~
	 * $(document).ajaxSuccess(function(event, xhr, settings) {
	 *     if (xhr.status == 278) {
	 *         window.location = xhr.getResponseHeader('Location');
	 *     }
	 * });
	 * ~~~
	 *
	 * @param array|string $url the URL to be redirected to. [[\yii\helpers\Html::url()]]
	 * will be used to normalize the URL. If the resulting URL is still a relative URL
	 * (one without host info), the current request host info will be used.
Qiang Xue committed
229
	 * @param boolean $terminate whether to terminate the current application
Qiang Xue committed
230 231
	 * @param integer $statusCode the HTTP status code. Defaults to 302.
	 * See [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html]]
Qiang Xue committed
232
	 * for details about HTTP status code.
Qiang Xue committed
233
	 * Note that if the request is an AJAX request, [[ajaxRedirectCode]] will be used instead.
Qiang Xue committed
234
	 */
Qiang Xue committed
235
	public function redirect($url, $terminate = true, $statusCode = 302)
Qiang Xue committed
236
	{
Qiang Xue committed
237
		$url = Html::url($url);
Qiang Xue committed
238 239 240
		if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) {
			$url = Yii::$app->getRequest()->getHostInfo() . $url;
		}
Qiang Xue committed
241 242 243
		if (Yii::$app->getRequest()->getIsAjaxRequest()) {
			$statusCode = $this->ajaxRedirectCode;
		}
Qiang Xue committed
244 245 246 247
		header('Location: ' . $url, true, $statusCode);
		if ($terminate) {
			Yii::$app->end();
		}
Qiang Xue committed
248
	}
249

250 251 252 253 254 255 256 257 258 259 260 261 262
	/**
	 * Refreshes the current page.
	 * The effect of this method call is the same as the user pressing the refresh button of his browser
	 * (without re-posting data).
	 * @param boolean $terminate whether to terminate the current application after calling this method
	 * @param string $anchor the anchor that should be appended to the redirection URL.
	 * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it.
	 */
	public function refresh($terminate = true, $anchor = '')
	{
		$this->redirect(Yii::$app->getRequest()->getUrl() . $anchor, $terminate);
	}

263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
	/**
	 * Returns the cookie collection.
	 * Through the returned cookie collection, you add or remove cookies as follows,
	 *
	 * ~~~
	 * // add a cookie
	 * $response->cookies->add(new Cookie(array(
	 *     'name' => $name,
	 *     'value' => $value,
	 * ));
	 *
	 * // remove a cookie
	 * $response->cookies->remove('name');
	 * // alternatively
	 * unset($response->cookies['name']);
	 * ~~~
	 *
	 * @return CookieCollection the cookie collection.
	 */
	public function getCookies()
	{
Qiang Xue committed
284
		return Yii::$app->getRequest()->getCookies();
285
	}
Qiang Xue committed
286
}