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-2012, 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-2012, 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: * If not provided its extracted from filename provided as `id`.
36: * - `path` The absolute path, including the trailing / on the server's filesystem to `id`.
37: * - `mimeType` The mime type of the file if CakeResponse doesn't know about it.
38: * Must be an associative array with extension as key and mime type as value eg. array('ini' => 'text/plain')
39: *
40: * ### Usage
41: *
42: * {{{
43: * class ExampleController extends AppController {
44: * public function download() {
45: * $this->viewClass = 'Media';
46: * $params = array(
47: * 'id' => 'example.zip',
48: * 'name' => 'example',
49: * 'download' => true,
50: * 'extension' => 'zip',
51: * 'path' => APP . 'files' . DS
52: * );
53: * $this->set($params);
54: * }
55: * }
56: * }}}
57: *
58: * @package Cake.View
59: */
60: class MediaView extends View {
61:
62: /**
63: * Indicates whether response gzip compression was enabled for this class
64: *
65: * @var boolean
66: */
67: protected $_compressionEnabled = false;
68:
69: /**
70: * Display or download the given file
71: *
72: * @param string $view Not used
73: * @param string $layout Not used
74: * @return mixed
75: * @throws NotFoundException
76: */
77: public function render($view = null, $layout = null) {
78: $name = $download = $extension = $id = $modified = $path = $cache = $mimeType = $compress = null;
79: extract($this->viewVars, EXTR_OVERWRITE);
80:
81: if (is_dir($path)) {
82: $path = $path . $id;
83: } else {
84: $path = APP . $path . $id;
85: }
86:
87: if (!is_file($path)) {
88: if (Configure::read('debug')) {
89: throw new NotFoundException(sprintf('The requested file %s was not found', $path));
90: }
91: throw new NotFoundException('The requested file was not found');
92: }
93:
94: if (is_array($mimeType)) {
95: $this->response->type($mimeType);
96: }
97:
98: if (!isset($extension)) {
99: $extension = pathinfo($id, PATHINFO_EXTENSION);
100: }
101:
102: if ($this->_isActive()) {
103: $extension = strtolower($extension);
104: $chunkSize = 8192;
105: $buffer = '';
106: $fileSize = @filesize($path);
107: $handle = fopen($path, 'rb');
108:
109: if ($handle === false) {
110: return false;
111: }
112: if (!empty($modified) && !is_numeric($modified)) {
113: $modified = strtotime($modified, time());
114: } else {
115: $modified = time();
116: }
117: if (!$extension || $this->response->type($extension) === false) {
118: $download = true;
119: }
120:
121: if ($cache) {
122: $this->response->cache($modified, $cache);
123: } else {
124: $this->response->header(array(
125: 'Date' => gmdate('D, d M Y H:i:s', time()) . ' GMT',
126: 'Expires' => '0',
127: 'Cache-Control' => 'private, must-revalidate, post-check=0, pre-check=0',
128: 'Pragma' => 'no-cache'
129: ));
130: }
131:
132: if ($download) {
133: $agent = env('HTTP_USER_AGENT');
134:
135: if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent)) {
136: $contentType = 'application/octetstream';
137: } elseif (preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) {
138: $contentType = 'application/force-download';
139: }
140:
141: if (!empty($contentType)) {
142: $this->response->type($contentType);
143: }
144: if (is_null($name)) {
145: $name = $id;
146: } elseif ($extension) {
147: $name .= '.' . $extension;
148: }
149: $this->response->download($name);
150: $this->response->header(array('Accept-Ranges' => 'bytes'));
151:
152: $httpRange = env('HTTP_RANGE');
153: if (isset($httpRange)) {
154: list($toss, $range) = explode('=', $httpRange);
155:
156: $size = $fileSize - 1;
157: $length = $fileSize - $range;
158:
159: $this->response->header(array(
160: 'Content-Length' => $length,
161: 'Content-Range' => 'bytes ' . $range . $size . '/' . $fileSize
162: ));
163:
164: $this->response->statusCode(206);
165: fseek($handle, $range);
166: } else {
167: $this->response->header('Content-Length', $fileSize);
168: }
169: } else {
170: $this->response->header(array(
171: 'Content-Length' => $fileSize
172: ));
173: }
174: $this->_clearBuffer();
175: if ($compress) {
176: $this->_compressionEnabled = $this->response->compress();
177: }
178:
179: $this->response->send();
180: return $this->_sendFile($handle);
181: }
182:
183: return false;
184: }
185:
186: /**
187: * Reads out a file handle, and echos the content to the client.
188: *
189: * @param resource $handle A file handle or stream
190: * @return void
191: */
192: protected function _sendFile($handle) {
193: $chunkSize = 8192;
194: $buffer = '';
195: while (!feof($handle)) {
196: if (!$this->_isActive()) {
197: fclose($handle);
198: return false;
199: }
200: set_time_limit(0);
201: $buffer = fread($handle, $chunkSize);
202: echo $buffer;
203: if (!$this->_compressionEnabled) {
204: $this->_flushBuffer();
205: }
206: }
207: fclose($handle);
208: }
209:
210: /**
211: * Returns true if connection is still active
212: *
213: * @return boolean
214: */
215: protected function _isActive() {
216: return connection_status() == 0 && !connection_aborted();
217: }
218:
219: /**
220: * Clears the contents of the topmost output buffer and discards them
221: *
222: * @return boolean
223: */
224: protected function _clearBuffer() {
225: return @ob_end_clean();
226: }
227:
228: /**
229: * Flushes the contents of the output buffer
230: *
231: * @return void
232: */
233: protected function _flushBuffer() {
234: @flush();
235: @ob_flush();
236: }
237:
238: }
239: