1: <?php
2: /**
3: * Methods to display or download any type of file
4: *
5: * PHP 5
6: *
7: * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
8: * Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
9: *
10: * Licensed under The MIT License
11: * Redistributions of files must retain the above copyright notice.
12: *
13: * @copyright Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
14: * @link http://cakephp.org CakePHP(tm) Project
15: * @package Cake.View
16: * @since CakePHP(tm) v 1.2.0.5714
17: * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
18: */
19:
20: App::uses('View', 'View');
21: App::uses('CakeRequest', 'Network');
22:
23: /**
24: * Media View provides a custom view implementation for sending files to visitors. Its great
25: * for making the response of a controller action be a file that is saved somewhere on the filesystem.
26: *
27: * An example use comes from the CakePHP internals. MediaView is used to serve plugin and theme assets,
28: * as they are not normally accessible from an application's webroot. Unlike other views, MediaView
29: * uses several viewVars that have special meaning:
30: *
31: * - `id` The filename on the server's filesystem, including extension.
32: * - `name` The filename that will be sent to the user, specified without the extension.
33: * - `download` Set to true to set a `Content-Disposition` header. This is ideal for file downloads.
34: * - `extension` The extension of the file being served. This is used to set the mimetype
35: * - `path` The absolute path, including the trailing / on the server's filesystem to `id`.
36: * - `mimeType` The mime type of the file if CakeResponse doesn't know about it.
37: *
38: * ### Usage
39: *
40: * {{{
41: * class ExampleController extends AppController {
42: * public function download () {
43: * $this->viewClass = 'Media';
44: * $params = array(
45: * 'id' => 'example.zip',
46: * 'name' => 'example',
47: * 'download' => true,
48: * 'extension' => 'zip',
49: * 'path' => APP . 'files' . DS
50: * );
51: * $this->set($params);
52: * }
53: * }
54: * }}}
55: *
56: * @package Cake.View
57: */
58: class MediaView extends View {
59: /**
60: * Indicates whether response gzip compression was enabled for this class
61: *
62: * @var boolean
63: */
64: protected $_compressionEnabled = false;
65:
66: /**
67: * Reference to the Response object responsible for sending the headers
68: *
69: * @var CakeResponse
70: */
71: public $response = null;
72:
73: /**
74: * Constructor
75: *
76: * @param Controller $controller The controller with viewVars
77: */
78: public function __construct($controller = null) {
79: parent::__construct($controller);
80: if (is_object($controller) && isset($controller->response)) {
81: $this->response = $controller->response;
82: } else {
83: $this->response = new CakeResponse;
84: }
85: }
86:
87: /**
88: * Display or download the given file
89: *
90: * @param string $view Not used
91: * @param string $layout Not used
92: * @return mixed
93: * @throws NotFoundException
94: */
95: public function render($view = null, $layout = null) {
96: $name = $download = $extension = $id = $modified = $path = $cache = $mimeType = $compress = null;
97: extract($this->viewVars, EXTR_OVERWRITE);
98:
99: if (is_dir($path)) {
100: $path = $path . $id;
101: } else {
102: $path = APP . $path . $id;
103: }
104:
105: if (!is_file($path)) {
106: if (Configure::read('debug')) {
107: throw new NotFoundException(sprintf('The requested file %s was not found', $path));
108: }
109: throw new NotFoundException('The requested file was not found');
110: }
111:
112: if (is_array($mimeType)) {
113: $this->response->type($mimeType);
114: }
115:
116: if (isset($extension) && $this->_isActive()) {
117: $extension = strtolower($extension);
118: $chunkSize = 8192;
119: $buffer = '';
120: $fileSize = @filesize($path);
121: $handle = fopen($path, 'rb');
122:
123: if ($handle === false) {
124: return false;
125: }
126: if (!empty($modified) && !is_numeric($modified)) {
127: $modified = strtotime($modified, time());
128: } else {
129: $modified = time();
130: }
131: if ($this->response->type($extension) === false) {
132: $download = true;
133: }
134:
135: if ($cache) {
136: $this->response->cache($modified, $cache);
137: } else {
138: $this->response->header(array(
139: 'Date' => gmdate('D, d M Y H:i:s', time()) . ' GMT',
140: 'Expires' => '0',
141: 'Cache-Control' => 'private, must-revalidate, post-check=0, pre-check=0',
142: 'Pragma' => 'no-cache'
143: ));
144: }
145:
146: if ($download) {
147: $agent = env('HTTP_USER_AGENT');
148:
149: if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent)) {
150: $contentType = 'application/octetstream';
151: } else if (preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) {
152: $contentType = 'application/force-download';
153: }
154:
155: if (!empty($contentType)) {
156: $this->response->type($contentType);
157: }
158: if (is_null($name)) {
159: $name = $id;
160: }
161: $this->response->download($name);
162: $this->response->header(array('Accept-Ranges' => 'bytes'));
163:
164: $httpRange = env('HTTP_RANGE');
165: if (isset($httpRange)) {
166: list($toss, $range) = explode('=', $httpRange);
167:
168: $size = $fileSize - 1;
169: $length = $fileSize - $range;
170:
171: $this->response->header(array(
172: 'Content-Length' => $length,
173: 'Content-Range' => 'bytes ' . $range . $size . '/' . $fileSize
174: ));
175:
176: $this->response->statusCode(206);
177: fseek($handle, $range);
178: } else {
179: $this->response->header('Content-Length', $fileSize);
180: }
181: } else {
182: $this->response->header(array(
183: 'Content-Length' => $fileSize
184: ));
185: }
186: $this->_clearBuffer();
187: if ($compress) {
188: $this->_compressionEnabled = $this->response->compress();
189: }
190:
191: $this->response->send();
192: return $this->_sendFile($handle);
193: }
194:
195: return false;
196: }
197:
198: /**
199: * Reads out a file handle, and echos the content to the client.
200: *
201: * @param resource $handle A file handle or stream
202: * @return void
203: */
204: protected function _sendFile($handle) {
205: $chunkSize = 8192;
206: $buffer = '';
207: while (!feof($handle)) {
208: if (!$this->_isActive()) {
209: fclose($handle);
210: return false;
211: }
212: set_time_limit(0);
213: $buffer = fread($handle, $chunkSize);
214: echo $buffer;
215: if (!$this->_compressionEnabled) {
216: $this->_flushBuffer();
217: }
218: }
219: fclose($handle);
220: }
221:
222: /**
223: * Returns true if connection is still active
224: *
225: * @return boolean
226: */
227: protected function _isActive() {
228: return connection_status() == 0 && !connection_aborted();
229: }
230:
231: /**
232: * Clears the contents of the topmost output buffer and discards them
233: *
234: * @return boolean
235: */
236: protected function _clearBuffer() {
237: return @ob_end_clean();
238: }
239:
240: /**
241: * Flushes the contents of the output buffer
242: *
243: * @return void
244: */
245: protected function _flushBuffer() {
246: @flush();
247: @ob_flush();
248: }
249: }
250: