1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @author Arthur Schiwon <[email protected]> |
4
|
|
|
* @author Bart Visscher <[email protected]> |
5
|
|
|
* @author Björn Schießle <[email protected]> |
6
|
|
|
* @author Clark Tomlinson <[email protected]> |
7
|
|
|
* @author Frank Karlitschek <[email protected]> |
8
|
|
|
* @author Jakob Sack <[email protected]> |
9
|
|
|
* @author Joas Schilling <[email protected]> |
10
|
|
|
* @author Jörn Friedrich Dreyer <[email protected]> |
11
|
|
|
* @author Lukas Reschke <[email protected]> |
12
|
|
|
* @author Michael Gapczynski <[email protected]> |
13
|
|
|
* @author Nicolai Ehemann <[email protected]> |
14
|
|
|
* @author Piotr Filiciak <[email protected]> |
15
|
|
|
* @author Robin Appelman <[email protected]> |
16
|
|
|
* @author Robin McCorkell <[email protected]> |
17
|
|
|
* @author Thibaut GRIDEL <[email protected]> |
18
|
|
|
* @author Thomas Müller <[email protected]> |
19
|
|
|
* @author Victor Dubiniuk <[email protected]> |
20
|
|
|
* @author Vincent Petry <[email protected]> |
21
|
|
|
* |
22
|
|
|
* @copyright Copyright (c) 2018, ownCloud GmbH |
23
|
|
|
* @license AGPL-3.0 |
24
|
|
|
* |
25
|
|
|
* This code is free software: you can redistribute it and/or modify |
26
|
|
|
* it under the terms of the GNU Affero General Public License, version 3, |
27
|
|
|
* as published by the Free Software Foundation. |
28
|
|
|
* |
29
|
|
|
* This program is distributed in the hope that it will be useful, |
30
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
31
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
32
|
|
|
* GNU Affero General Public License for more details. |
33
|
|
|
* |
34
|
|
|
* You should have received a copy of the GNU Affero General Public License, version 3, |
35
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/> |
36
|
|
|
* |
37
|
|
|
*/ |
38
|
|
|
|
39
|
|
|
use OC\Files\View; |
40
|
|
|
use OC\Streamer; |
41
|
|
|
use OCP\Lock\ILockingProvider; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Class for file server access |
45
|
|
|
* |
46
|
|
|
*/ |
47
|
|
|
class OC_Files { |
48
|
|
|
const FILE = 1; |
49
|
|
|
const ZIP_FILES = 2; |
50
|
|
|
const ZIP_DIR = 3; |
51
|
|
|
|
52
|
|
|
const UPLOAD_MIN_LIMIT_BYTES = 1048576; // 1 MiB |
53
|
|
|
|
54
|
|
|
private static $multipartBoundary = ''; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @return string |
58
|
|
|
*/ |
59
|
|
|
private static function getBoundary() { |
60
|
|
|
if (empty(self::$multipartBoundary)) { |
61
|
|
|
self::$multipartBoundary = \md5(\mt_rand()); |
62
|
|
|
} |
63
|
|
|
return self::$multipartBoundary; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @param string $filename |
68
|
|
|
* @param string $name |
69
|
|
|
* @param array $rangeArray ('from'=>int,'to'=>int), ... |
70
|
|
|
*/ |
71
|
|
|
private static function sendHeaders($filename, $name, array $rangeArray) { |
72
|
|
|
OC_Response::setContentDispositionHeader($name, 'attachment'); |
73
|
|
|
\header('Content-Transfer-Encoding: binary', true); |
74
|
|
|
OC_Response::disableCaching(); |
75
|
|
|
$fileSize = \OC\Files\Filesystem::filesize($filename); |
76
|
|
|
$type = \OC::$server->getMimeTypeDetector()->getSecureMimeType(\OC\Files\Filesystem::getMimeType($filename)); |
77
|
|
|
if ($fileSize > -1) { |
78
|
|
|
if (!empty($rangeArray)) { |
79
|
|
|
\header('HTTP/1.1 206 Partial Content', true); |
80
|
|
|
\header('Accept-Ranges: bytes', true); |
81
|
|
|
if (\count($rangeArray) > 1) { |
82
|
|
|
$type = 'multipart/byteranges; boundary='.self::getBoundary(); |
83
|
|
|
// no Content-Length header here |
84
|
|
|
} else { |
85
|
|
|
\header(\sprintf('Content-Range: bytes %d-%d/%d', $rangeArray[0]['from'], $rangeArray[0]['to'], $fileSize), true); |
86
|
|
|
OC_Response::setContentLengthHeader($rangeArray[0]['to'] - $rangeArray[0]['from'] + 1); |
87
|
|
|
} |
88
|
|
|
} else { |
89
|
|
|
OC_Response::setContentLengthHeader($fileSize); |
90
|
|
|
} |
91
|
|
|
} |
92
|
|
|
\header('Content-Type: '.$type, true); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* return the content of a file or return a zip file containing multiple files |
97
|
|
|
* |
98
|
|
|
* @param string $dir |
99
|
|
|
* @param string $files ; separated list of files to download |
100
|
|
|
* @param array $params ; 'head' boolean to only send header of the request ; 'range' http range header |
101
|
|
|
*/ |
102
|
|
|
public static function get($dir, $files, $params = null) { |
103
|
|
|
$view = \OC\Files\Filesystem::getView(); |
104
|
|
|
$getType = self::FILE; |
105
|
|
|
$filename = $dir; |
106
|
|
|
try { |
107
|
|
|
if (\is_array($files) && \count($files) === 1) { |
108
|
|
|
$files = $files[0]; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
if (!\is_array($files)) { |
112
|
|
|
$filename = $dir . '/' . $files; |
113
|
|
|
if (!$view->is_dir($filename)) { |
114
|
|
|
self::getSingleFile($view, $dir, $files, $params === null ? [] : $params); |
115
|
|
|
return; |
116
|
|
|
} |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
$name = 'download'; |
120
|
|
|
if (\is_array($files)) { |
121
|
|
|
$getType = self::ZIP_FILES; |
122
|
|
|
$basename = \basename($dir); |
123
|
|
|
if ($basename) { |
124
|
|
|
$name = $basename; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
$filename = $dir . '/' . $name; |
128
|
|
|
} else { |
129
|
|
|
$filename = $dir . '/' . $files; |
130
|
|
|
$getType = self::ZIP_DIR; |
131
|
|
|
// downloading root ? |
132
|
|
|
if ($files !== '') { |
133
|
|
|
$name = $files; |
134
|
|
|
} |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
//Dispatch an event to see if any apps have problem with download |
138
|
|
|
$event = new \Symfony\Component\EventDispatcher\GenericEvent(null, ['dir' => $dir, 'files' => $files, 'run' => true]); |
139
|
|
|
OC::$server->getEventDispatcher()->dispatch('file.beforeCreateZip', $event); |
140
|
|
|
if (($event->getArgument('run') === false) or ($event->hasArgument('errorMessage'))) { |
141
|
|
|
throw new \OC\ForbiddenException("Access denied: " . $event->getArgument('errorMessage')); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
$streamer = new Streamer(); |
145
|
|
|
OC_Util::obEnd(); |
146
|
|
|
|
147
|
|
|
self::lockFiles($view, $dir, $files); |
148
|
|
|
|
149
|
|
|
$streamer->sendHeaders($name); |
150
|
|
|
$executionTime = \intval(OC::$server->getIniWrapper()->getNumeric('max_execution_time')); |
151
|
|
|
\set_time_limit(0); |
152
|
|
|
\ignore_user_abort(true); |
153
|
|
|
if ($getType === self::ZIP_FILES) { |
154
|
|
|
foreach ($files as $file) { |
155
|
|
|
$file = $dir . '/' . $file; |
156
|
|
View Code Duplication |
if (\OC\Files\Filesystem::is_file($file)) { |
|
|
|
|
157
|
|
|
$fileSize = \OC\Files\Filesystem::filesize($file); |
158
|
|
|
$fh = \OC\Files\Filesystem::fopen($file, 'r'); |
159
|
|
|
$streamer->addFileFromStream($fh, \basename($file), $fileSize); |
|
|
|
|
160
|
|
|
\fclose($fh); |
161
|
|
|
} elseif (\OC\Files\Filesystem::is_dir($file)) { |
162
|
|
|
$streamer->addDirRecursive($file); |
163
|
|
|
} |
164
|
|
|
} |
165
|
|
|
} elseif ($getType === self::ZIP_DIR) { |
166
|
|
|
$file = $dir . '/' . $files; |
167
|
|
|
$streamer->addDirRecursive($file); |
168
|
|
|
} |
169
|
|
|
$streamer->finalize(); |
170
|
|
|
\set_time_limit($executionTime); |
171
|
|
|
self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); |
172
|
|
|
$event = new \Symfony\Component\EventDispatcher\GenericEvent(null, ['result' => 'success', 'dir' => $dir, 'files' => $files]); |
173
|
|
|
OC::$server->getEventDispatcher()->dispatch('file.afterCreateZip', $event); |
174
|
|
|
} catch (\OCP\Lock\LockedException $ex) { |
175
|
|
|
self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); |
176
|
|
|
OC::$server->getLogger()->logException($ex); |
177
|
|
|
$l = \OC::$server->getL10N('core'); |
178
|
|
|
$hint = \method_exists($ex, 'getHint') ? $ex->getHint() : ''; |
179
|
|
|
\OC_Template::printErrorPage($l->t('File is currently busy, please try again later'), $hint); |
180
|
|
|
} catch (\OCP\Files\ForbiddenException $ex) { |
181
|
|
|
self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); |
182
|
|
|
OC::$server->getLogger()->logException($ex); |
183
|
|
|
$l = \OC::$server->getL10N('core'); |
184
|
|
|
\OC_Template::printErrorPage($l->t('File cannot be read'), $ex->getMessage(), 403); |
185
|
|
|
} catch (\Exception $ex) { |
186
|
|
|
self::unlockAllTheFiles($dir, $files, $getType, $view, $filename); |
187
|
|
|
OC::$server->getLogger()->logException($ex); |
188
|
|
|
$l = \OC::$server->getL10N('core'); |
189
|
|
|
$hint = \method_exists($ex, 'getHint') ? $ex->getHint() : ''; |
|
|
|
|
190
|
|
|
if ($event->hasArgument('message')) { |
191
|
|
|
$hint .= ' ' . $event->getArgument('message'); |
|
|
|
|
192
|
|
|
} |
193
|
|
|
\OC_Template::printErrorPage($l->t('File cannot be read'), $hint); |
194
|
|
|
} |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* @param string $rangeHeaderPos |
199
|
|
|
* @param int $fileSize |
200
|
|
|
* @return array $rangeArray ('from'=>int,'to'=>int), ... |
201
|
|
|
*/ |
202
|
|
|
private static function parseHttpRangeHeader($rangeHeaderPos, $fileSize) { |
203
|
|
|
$rArray=\explode(',', $rangeHeaderPos); |
204
|
|
|
$minOffset = 0; |
205
|
|
|
$ind = 0; |
206
|
|
|
|
207
|
|
|
$rangeArray = []; |
208
|
|
|
|
209
|
|
|
foreach ($rArray as $value) { |
210
|
|
|
$ranges = \explode('-', $value); |
211
|
|
|
if (\is_numeric($ranges[0])) { |
212
|
|
|
if ($ranges[0] < $minOffset) { // case: bytes=500-700,601-999 |
213
|
|
|
$ranges[0] = $minOffset; |
214
|
|
|
} |
215
|
|
|
if ($ind > 0 && $ranges[0] == $rangeArray[$ind-1]['to']+1) { // case: bytes=500-600,601-999 |
216
|
|
|
$ind--; |
217
|
|
|
$ranges[0] = $rangeArray[$ind]['from']; |
218
|
|
|
} |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
if (\is_numeric($ranges[0]) && \is_numeric($ranges[1]) && $ranges[0] < $fileSize && $ranges[0] <= $ranges[1]) { |
222
|
|
|
// case: x-x |
223
|
|
|
if ($ranges[1] >= $fileSize) { |
224
|
|
|
$ranges[1] = $fileSize-1; |
225
|
|
|
} |
226
|
|
|
$rangeArray[$ind++] = ['from' => $ranges[0], 'to' => $ranges[1], 'size' => $fileSize]; |
227
|
|
|
$minOffset = $ranges[1] + 1; |
228
|
|
|
if ($minOffset >= $fileSize) { |
229
|
|
|
break; |
230
|
|
|
} |
231
|
|
|
} elseif (\is_numeric($ranges[0]) && $ranges[0] < $fileSize) { |
232
|
|
|
// case: x- |
233
|
|
|
$rangeArray[$ind++] = ['from' => $ranges[0], 'to' => $fileSize-1, 'size' => $fileSize]; |
234
|
|
|
break; |
235
|
|
|
} elseif (\is_numeric($ranges[1])) { |
236
|
|
|
// case: -x |
237
|
|
|
if ($ranges[1] > $fileSize) { |
238
|
|
|
$ranges[1] = $fileSize; |
239
|
|
|
} |
240
|
|
|
$rangeArray[$ind++] = ['from' => $fileSize-$ranges[1], 'to' => $fileSize-1, 'size' => $fileSize]; |
241
|
|
|
break; |
242
|
|
|
} |
243
|
|
|
} |
244
|
|
|
return $rangeArray; |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* @param View $view |
249
|
|
|
* @param string $name |
250
|
|
|
* @param string $dir |
251
|
|
|
* @param array $params ; 'head' boolean to only send header of the request ; 'range' http range header |
252
|
|
|
*/ |
253
|
|
|
private static function getSingleFile($view, $dir, $name, $params) { |
254
|
|
|
$filename = $dir . '/' . $name; |
255
|
|
|
OC_Util::obEnd(); |
256
|
|
|
$view->lockFile($filename, ILockingProvider::LOCK_SHARED); |
257
|
|
|
|
258
|
|
|
$rangeArray = []; |
259
|
|
|
|
260
|
|
|
if (isset($params['range']) && \substr($params['range'], 0, 6) === 'bytes=') { |
261
|
|
|
$rangeArray = self::parseHttpRangeHeader(\substr($params['range'], 6), |
262
|
|
|
\OC\Files\Filesystem::filesize($filename)); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
if (\OC\Files\Filesystem::isReadable($filename)) { |
266
|
|
|
self::sendHeaders($filename, $name, $rangeArray); |
267
|
|
|
} elseif (!\OC\Files\Filesystem::file_exists($filename)) { |
268
|
|
|
\header("HTTP/1.1 404 Not Found"); |
269
|
|
|
$tmpl = new OC_Template('', '404', 'guest'); |
270
|
|
|
$tmpl->printPage(); |
271
|
|
|
exit(); |
272
|
|
|
} else { |
273
|
|
|
\header("HTTP/1.1 403 Forbidden"); |
274
|
|
|
die('403 Forbidden'); |
275
|
|
|
} |
276
|
|
|
if (isset($params['head']) && $params['head']) { |
277
|
|
|
return; |
278
|
|
|
} |
279
|
|
|
if (!empty($rangeArray)) { |
280
|
|
|
try { |
281
|
|
|
if (\count($rangeArray) == 1) { |
282
|
|
|
$view->readfilePart($filename, $rangeArray[0]['from'], $rangeArray[0]['to']); |
283
|
|
|
} else { |
284
|
|
|
// check if file is seekable (if not throw UnseekableException) |
285
|
|
|
// we have to check it before body contents |
286
|
|
|
$view->readfilePart($filename, $rangeArray[0]['size'], $rangeArray[0]['size']); |
287
|
|
|
|
288
|
|
|
$type = \OC::$server->getMimeTypeDetector()->getSecureMimeType(\OC\Files\Filesystem::getMimeType($filename)); |
289
|
|
|
|
290
|
|
|
foreach ($rangeArray as $range) { |
291
|
|
|
echo "\r\n--".self::getBoundary()."\r\n". |
292
|
|
|
"Content-type: ".$type."\r\n". |
293
|
|
|
"Content-range: bytes ".$range['from']."-".$range['to']."/".$range['size']."\r\n\r\n"; |
294
|
|
|
$view->readfilePart($filename, $range['from'], $range['to']); |
295
|
|
|
} |
296
|
|
|
echo "\r\n--".self::getBoundary()."--\r\n"; |
297
|
|
|
} |
298
|
|
|
} catch (\OCP\Files\UnseekableException $ex) { |
299
|
|
|
// file is unseekable |
300
|
|
|
\header_remove('Accept-Ranges'); |
301
|
|
|
\header_remove('Content-Range'); |
302
|
|
|
\header("HTTP/1.1 200 OK"); |
303
|
|
|
self::sendHeaders($filename, $name, []); |
304
|
|
|
$view->readfile($filename); |
305
|
|
|
} |
306
|
|
|
} else { |
307
|
|
|
$view->readfile($filename); |
308
|
|
|
} |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* @param View $view |
313
|
|
|
* @param string $dir |
314
|
|
|
* @param string[]|string $files |
315
|
|
|
*/ |
316
|
|
|
public static function lockFiles($view, $dir, $files) { |
317
|
|
|
if (!\is_array($files)) { |
318
|
|
|
$file = $dir . '/' . $files; |
319
|
|
|
$files = [$file]; |
320
|
|
|
} |
321
|
|
|
foreach ($files as $file) { |
322
|
|
|
$file = $dir . '/' . $file; |
323
|
|
|
$view->lockFile($file, ILockingProvider::LOCK_SHARED); |
324
|
|
|
if ($view->is_dir($file)) { |
325
|
|
|
$contents = $view->getDirectoryContent($file); |
326
|
|
|
$contents = \array_map(function ($fileInfo) use ($file) { |
327
|
|
|
/** @var \OCP\Files\FileInfo $fileInfo */ |
328
|
|
|
return $file . '/' . $fileInfo->getName(); |
329
|
|
|
}, $contents); |
330
|
|
|
self::lockFiles($view, $dir, $contents); |
331
|
|
|
} |
332
|
|
|
} |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* @param string $dir |
337
|
|
|
* @param $files |
338
|
|
|
* @param integer $getType |
339
|
|
|
* @param View $view |
340
|
|
|
* @param string $filename |
341
|
|
|
*/ |
342
|
|
|
private static function unlockAllTheFiles($dir, $files, $getType, $view, $filename) { |
343
|
|
|
if ($getType === self::FILE) { |
344
|
|
|
$view->unlockFile($filename, ILockingProvider::LOCK_SHARED); |
345
|
|
|
} |
346
|
|
View Code Duplication |
if ($getType === self::ZIP_FILES) { |
|
|
|
|
347
|
|
|
foreach ($files as $file) { |
348
|
|
|
$file = $dir . '/' . $file; |
349
|
|
|
$view->unlockFile($file, ILockingProvider::LOCK_SHARED); |
350
|
|
|
} |
351
|
|
|
} |
352
|
|
View Code Duplication |
if ($getType === self::ZIP_DIR) { |
|
|
|
|
353
|
|
|
$file = $dir . '/' . $files; |
354
|
|
|
$view->unlockFile($file, ILockingProvider::LOCK_SHARED); |
355
|
|
|
} |
356
|
|
|
} |
357
|
|
|
} |
358
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.