Response.php 11 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

/**
 * @author Qiang Xue <qiang.xue@gmail.com>
18
 * @author Carsten Brandt <mail@cebe.cc>
Qiang Xue committed
19 20 21 22
 * @since 2.0
 */
class Response extends \yii\base\Response
{
Qiang Xue committed
23 24 25 26 27 28 29 30
	/**
	 * @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
31 32 33 34 35 36
	/**
	 * 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
37
	 * @throws \yii\base\HttpException when range request is not satisfiable.
Qiang Xue committed
38 39 40
	 */
	public function sendFile($fileName, $content, $mimeType = null, $terminate = true)
	{
41 42
		if ($mimeType === null && (($mimeType = FileHelper::getMimeTypeByExtension($fileName)) === null)) {
			$mimeType = 'application/octet-stream';
Qiang Xue committed
43
		}
44

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

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

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

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

61
			// range requests starts from "-", so it means that data must be dumped the end point.
62
			if ($range[0] === '-') {
63
				$contentStart = $fileSize - substr($range, 1);
64
			} else {
65
				$range = explode('-', $range);
66
				$contentStart = $range[0];
67 68 69 70 71

				// check if the last-byte-pos presents in header
				if ((isset($range[1]) && is_numeric($range[1]))) {
					$contentEnd = $range[1];
				}
72 73 74 75 76 77
			}

			/* 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.
78
			$contentEnd = ($contentEnd > $fileSize) ? $fileSize -1 : $contentEnd;
79 80 81 82 83 84

			// 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");
85
				throw new HttpException(416, 'Requested Range Not Satisfiable');
86 87 88 89 90 91 92 93 94 95
			}

			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
96 97 98
		header('Pragma: public');
		header('Expires: 0');
		header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
99 100 101
		header('Content-Type: ' . $mimeType);
		header('Content-Length: ' . $length);
		header('Content-Disposition: attachment; filename="' . $fileName . '"');
Qiang Xue committed
102
		header('Content-Transfer-Encoding: binary');
103
		$content = StringHelper::substr($content, $contentStart, $length);
Qiang Xue committed
104 105 106 107

		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)
108
			ob_start();
109
			Yii::$app->end(0, false);
110
			ob_end_clean();
Qiang Xue committed
111 112
			echo $content;
			exit(0);
Ragazzo committed
113
		} else {
114
			echo $content;
Ragazzo committed
115
		}
Qiang Xue committed
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 144 145 146 147 148
	}

	/**
	 * 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
149 150 151 152 153
	 * <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
154 155 156
	 * <b>Example</b>:
	 * <pre>
	 * <?php
resurtm committed
157 158 159 160
	 *    Yii::app()->request->xSendFile('/home/user/Pictures/picture1.jpg', array(
	 *        'saveName' => 'image1.jpg',
	 *        'mimeType' => 'image/jpeg',
	 *        'terminate' => false,
Qiang Xue committed
161 162 163 164 165 166 167 168 169 170
	 *    ));
	 * ?>
	 * </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>
171 172
	 * <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
173
	 * </ul>
174
	 * @todo
Qiang Xue committed
175
	 */
Qiang Xue committed
176
	public function xSendFile($filePath, $options = array())
Qiang Xue committed
177
	{
Qiang Xue committed
178 179 180 181 182
		if (!isset($options['forceDownload']) || $options['forceDownload']) {
			$disposition = 'attachment';
		} else {
			$disposition = 'inline';
		}
Qiang Xue committed
183

Qiang Xue committed
184 185 186
		if (!isset($options['saveName'])) {
			$options['saveName'] = basename($filePath);
		}
Qiang Xue committed
187

Qiang Xue committed
188 189 190 191
		if (!isset($options['mimeType'])) {
			if (($options['mimeType'] = CFileHelper::getMimeTypeByExtension($filePath)) === null) {
				$options['mimeType'] = 'text/plain';
			}
Qiang Xue committed
192 193
		}

Qiang Xue committed
194 195 196
		if (!isset($options['xHeader'])) {
			$options['xHeader'] = 'X-Sendfile';
		}
Qiang Xue committed
197

Qiang Xue committed
198 199 200 201 202 203 204 205
		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
206
		}
Qiang Xue committed
207
		header(trim($options['xHeader']) . ': ' . $filePath);
Qiang Xue committed
208

Qiang Xue committed
209 210 211
		if (!isset($options['terminate']) || $options['terminate']) {
			Yii::$app->end();
		}
Qiang Xue committed
212
	}
Qiang Xue committed
213 214 215

	/**
	 * Redirects the browser to the specified URL.
Qiang Xue committed
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
	 * 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
234
	 * @param boolean $terminate whether to terminate the current application
Qiang Xue committed
235 236
	 * @param integer $statusCode the HTTP status code. Defaults to 302.
	 * See [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html]]
Qiang Xue committed
237
	 * for details about HTTP status code.
Qiang Xue committed
238
	 * Note that if the request is an AJAX request, [[ajaxRedirectCode]] will be used instead.
Qiang Xue committed
239
	 */
Qiang Xue committed
240
	public function redirect($url, $terminate = true, $statusCode = 302)
Qiang Xue committed
241
	{
Qiang Xue committed
242
		$url = Html::url($url);
Qiang Xue committed
243 244 245
		if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) {
			$url = Yii::$app->getRequest()->getHostInfo() . $url;
		}
Qiang Xue committed
246 247 248
		if (Yii::$app->getRequest()->getIsAjaxRequest()) {
			$statusCode = $this->ajaxRedirectCode;
		}
Qiang Xue committed
249 250 251 252
		header('Location: ' . $url, true, $statusCode);
		if ($terminate) {
			Yii::$app->end();
		}
Qiang Xue committed
253
	}
254

255 256 257 258 259 260 261 262 263 264 265 266 267
	/**
	 * 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);
	}

268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
	/**
	 * 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
289
		return Yii::$app->getRequest()->getCookies();
290
	}
Qiang Xue committed
291
}