Passed
Push — master ( 59402b...899021 )
by John
11:15 queued 10s
created

OC_Files   F

Complexity

Total Complexity 73

Size/Duplication

Total Lines 379
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 73
eloc 221
c 2
b 0
f 0
dl 0
loc 379
rs 2.56

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getBoundary() 0 5 2
A sendHeaders() 0 26 4
A lockFiles() 0 15 4
A unlockAllTheFiles() 0 13 5
F get() 0 117 25
C getSingleFile() 0 71 14
A getNumberOfFiles() 0 14 3
C parseHttpRangeHeader() 0 45 16

How to fix   Complexity   

Complex Class

Complex classes like OC_Files often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OC_Files, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Björn Schießle <[email protected]>
7
 * @author Daniel Calviño Sánchez <[email protected]>
8
 * @author Frank Karlitschek <[email protected]>
9
 * @author Jakob Sack <[email protected]>
10
 * @author Joas Schilling <[email protected]>
11
 * @author Jörn Friedrich Dreyer <[email protected]>
12
 * @author Ko- <[email protected]>
13
 * @author Lukas Reschke <[email protected]>
14
 * @author Michael Gapczynski <[email protected]>
15
 * @author Morris Jobke <[email protected]>
16
 * @author Nicolai Ehemann <[email protected]>
17
 * @author Piotr Filiciak <[email protected]>
18
 * @author Robin Appelman <[email protected]>
19
 * @author Robin McCorkell <[email protected]>
20
 * @author Roeland Jago Douma <[email protected]>
21
 * @author Thibaut GRIDEL <[email protected]>
22
 * @author Thomas Müller <[email protected]>
23
 * @author Victor Dubiniuk <[email protected]>
24
 * @author Vincent Petry <[email protected]>
25
 *
26
 * @license AGPL-3.0
27
 *
28
 * This code is free software: you can redistribute it and/or modify
29
 * it under the terms of the GNU Affero General Public License, version 3,
30
 * as published by the Free Software Foundation.
31
 *
32
 * This program is distributed in the hope that it will be useful,
33
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
34
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
35
 * GNU Affero General Public License for more details.
36
 *
37
 * You should have received a copy of the GNU Affero General Public License, version 3,
38
 * along with this program. If not, see <http://www.gnu.org/licenses/>
39
 *
40
 */
41
42
use OC\Files\View;
43
use OC\Streamer;
44
use OCP\ILogger;
45
use OCP\Lock\ILockingProvider;
46
47
/**
48
 * Class for file server access
49
 *
50
 */
51
class OC_Files {
52
	const FILE = 1;
53
	const ZIP_FILES = 2;
54
	const ZIP_DIR = 3;
55
56
	const UPLOAD_MIN_LIMIT_BYTES = 1048576; // 1 MiB
57
58
59
	private static $multipartBoundary = '';
60
61
	/**
62
	 * @return string
63
	 */
64
	private static function getBoundary() {
65
		if (empty(self::$multipartBoundary)) {
66
			self::$multipartBoundary = md5(mt_rand());
67
		}
68
		return self::$multipartBoundary;
69
	}
70
71
	/**
72
	 * @param string $filename
73
	 * @param string $name
74
	 * @param array $rangeArray ('from'=>int,'to'=>int), ...
75
	 */
76
	private static function sendHeaders($filename, $name, array $rangeArray) {
77
		OC_Response::setContentDispositionHeader($name, 'attachment');
78
		header('Content-Transfer-Encoding: binary', true);
79
		header('Pragma: public');// enable caching in IE
80
		header('Expires: 0');
81
		header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
82
		$fileSize = \OC\Files\Filesystem::filesize($filename);
83
		$type = \OC::$server->getMimeTypeDetector()->getSecureMimeType(\OC\Files\Filesystem::getMimeType($filename));
84
		if ($fileSize > -1) {
85
			if (!empty($rangeArray)) {
86
			    http_response_code(206);
87
			    header('Accept-Ranges: bytes', true);
88
			    if (count($rangeArray) > 1) {
89
				$type = 'multipart/byteranges; boundary='.self::getBoundary();
90
				// no Content-Length header here
91
			    }
92
			    else {
93
				header(sprintf('Content-Range: bytes %d-%d/%d', $rangeArray[0]['from'], $rangeArray[0]['to'], $fileSize), true);
94
				OC_Response::setContentLengthHeader($rangeArray[0]['to'] - $rangeArray[0]['from'] + 1);
95
			    }
96
			}
97
			else {
98
			    OC_Response::setContentLengthHeader($fileSize);
99
			}
100
		}
101
		header('Content-Type: '.$type, true);
102
	}
103
104
	/**
105
	 * return the content of a file or return a zip file containing multiple files
106
	 *
107
	 * @param string $dir
108
	 * @param string $files ; separated list of files to download
109
	 * @param array $params ; 'head' boolean to only send header of the request ; 'range' http range header
110
	 */
111
	public static function get($dir, $files, $params = null) {
112
113
		$view = \OC\Files\Filesystem::getView();
114
		$getType = self::FILE;
115
		$filename = $dir;
116
		try {
117
118
			if (is_array($files) && count($files) === 1) {
0 ignored issues
show
introduced by
The condition is_array($files) is always false.
Loading history...
119
				$files = $files[0];
120
			}
121
122
			if (!is_array($files)) {
0 ignored issues
show
introduced by
The condition is_array($files) is always false.
Loading history...
123
				$filename = $dir . '/' . $files;
124
				if (!$view->is_dir($filename)) {
125
					self::getSingleFile($view, $dir, $files, is_null($params) ? array() : $params);
126
					return;
127
				}
128
			}
129
130
			$name = 'download';
131
			if (is_array($files)) {
0 ignored issues
show
introduced by
The condition is_array($files) is always false.
Loading history...
132
				$getType = self::ZIP_FILES;
133
				$basename = basename($dir);
134
				if ($basename) {
135
					$name = $basename;
136
				}
137
138
				$filename = $dir . '/' . $name;
139
			} else {
140
				$filename = $dir . '/' . $files;
141
				$getType = self::ZIP_DIR;
142
				// downloading root ?
143
				if ($files !== '') {
144
					$name = $files;
145
				}
146
			}
147
148
			self::lockFiles($view, $dir, $files);
149
150
			/* Calculate filesize and number of files */
151
			if ($getType === self::ZIP_FILES) {
0 ignored issues
show
introduced by
The condition $getType === self::ZIP_FILES is always false.
Loading history...
152
				$fileInfos = array();
153
				$fileSize = 0;
154
				foreach ($files as $file) {
155
					$fileInfo = \OC\Files\Filesystem::getFileInfo($dir . '/' . $file);
156
					$fileSize += $fileInfo->getSize();
157
					$fileInfos[] = $fileInfo;
158
				}
159
				$numberOfFiles = self::getNumberOfFiles($fileInfos);
160
			} elseif ($getType === self::ZIP_DIR) {
0 ignored issues
show
introduced by
The condition $getType === self::ZIP_DIR is always true.
Loading history...
161
				$fileInfo = \OC\Files\Filesystem::getFileInfo($dir . '/' . $files);
162
				$fileSize = $fileInfo->getSize();
163
				$numberOfFiles = self::getNumberOfFiles(array($fileInfo));
0 ignored issues
show
Bug introduced by
array($fileInfo) of type array<integer,OC\Files\FileInfo|false> is incompatible with the type OCP\Files\FileInfo[] expected by parameter $fileInfos of OC_Files::getNumberOfFiles(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

163
				$numberOfFiles = self::getNumberOfFiles(/** @scrutinizer ignore-type */ array($fileInfo));
Loading history...
164
			}
165
166
			$streamer = new Streamer(\OC::$server->getRequest(), $fileSize, $numberOfFiles);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $numberOfFiles does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $fileSize does not seem to be defined for all execution paths leading up to this point.
Loading history...
167
			OC_Util::obEnd();
168
169
			$streamer->sendHeaders($name);
170
			$executionTime = (int)OC::$server->getIniWrapper()->getNumeric('max_execution_time');
171
			if (strpos(@ini_get('disable_functions'), 'set_time_limit') === false) {
172
				@set_time_limit(0);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for set_time_limit(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

172
				/** @scrutinizer ignore-unhandled */ @set_time_limit(0);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
173
			}
174
			ignore_user_abort(true);
175
176
			if ($getType === self::ZIP_FILES) {
0 ignored issues
show
introduced by
The condition $getType === self::ZIP_FILES is always false.
Loading history...
177
				foreach ($files as $file) {
178
					$file = $dir . '/' . $file;
179
					if (\OC\Files\Filesystem::is_file($file)) {
180
						$userFolder = \OC::$server->getRootFolder()->get(\OC\Files\Filesystem::getRoot());
181
						$file = $userFolder->get($file);
182
						if($file instanceof \OC\Files\Node\File) {
183
							try {
184
								$fh = $file->fopen('r');
185
							} catch (\OCP\Files\NotPermittedException $e) {
186
								continue;
187
							}
188
							$fileSize = $file->getSize();
189
							$fileTime = $file->getMTime();
190
						} else {
191
							// File is not a file? …
192
							\OC::$server->getLogger()->debug(
193
								'File given, but no Node available. Name {file}',
194
								[ 'app' => 'files', 'file' => $file ]
195
							);
196
							continue;
197
						}
198
						$streamer->addFileFromStream($fh, $file->getName(), $fileSize, $fileTime);
199
						fclose($fh);
200
					} elseif (\OC\Files\Filesystem::is_dir($file)) {
201
						$streamer->addDirRecursive($file);
202
					}
203
				}
204
			} elseif ($getType === self::ZIP_DIR) {
0 ignored issues
show
introduced by
The condition $getType === self::ZIP_DIR is always true.
Loading history...
205
				$file = $dir . '/' . $files;
206
				$streamer->addDirRecursive($file);
207
			}
208
			$streamer->finalize();
209
			set_time_limit($executionTime);
210
			self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
211
		} catch (\OCP\Lock\LockedException $ex) {
212
			self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
213
			OC::$server->getLogger()->logException($ex);
214
			$l = \OC::$server->getL10N('core');
215
			$hint = method_exists($ex, 'getHint') ? $ex->getHint() : '';
216
			\OC_Template::printErrorPage($l->t('File is currently busy, please try again later'), $hint, 200);
217
		} catch (\OCP\Files\ForbiddenException $ex) {
218
			self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
219
			OC::$server->getLogger()->logException($ex);
220
			$l = \OC::$server->getL10N('core');
221
			\OC_Template::printErrorPage($l->t('Can\'t read file'), $ex->getMessage(), 200);
222
		} catch (\Exception $ex) {
223
			self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
224
			OC::$server->getLogger()->logException($ex);
225
			$l = \OC::$server->getL10N('core');
226
			$hint = method_exists($ex, 'getHint') ? $ex->getHint() : '';
227
			\OC_Template::printErrorPage($l->t('Can\'t read file'), $hint, 200);
228
		}
229
	}
230
231
	/**
232
	 * @param string $rangeHeaderPos
233
	 * @param int $fileSize
234
	 * @return array $rangeArray ('from'=>int,'to'=>int), ...
235
	 */
236
	private static function parseHttpRangeHeader($rangeHeaderPos, $fileSize) {
237
		$rArray=explode(',', $rangeHeaderPos);
238
		$minOffset = 0;
239
		$ind = 0;
240
241
		$rangeArray = array();
242
243
		foreach ($rArray as $value) {
244
			$ranges = explode('-', $value);
245
			if (is_numeric($ranges[0])) {
246
				if ($ranges[0] < $minOffset) { // case: bytes=500-700,601-999
247
					$ranges[0] = $minOffset;
248
				}
249
				if ($ind > 0 && $rangeArray[$ind-1]['to']+1 == $ranges[0]) { // case: bytes=500-600,601-999
250
					$ind--;
251
					$ranges[0] = $rangeArray[$ind]['from'];
252
				}
253
			}
254
255
			if (is_numeric($ranges[0]) && is_numeric($ranges[1]) && $ranges[0] < $fileSize && $ranges[0] <= $ranges[1]) {
256
				// case: x-x
257
				if ($ranges[1] >= $fileSize) {
258
					$ranges[1] = $fileSize-1;
259
				}
260
				$rangeArray[$ind++] = array( 'from' => $ranges[0], 'to' => $ranges[1], 'size' => $fileSize );
261
				$minOffset = $ranges[1] + 1;
262
				if ($minOffset >= $fileSize) {
263
					break;
264
				}
265
			}
266
			elseif (is_numeric($ranges[0]) && $ranges[0] < $fileSize) {
267
				// case: x-
268
				$rangeArray[$ind++] = array( 'from' => $ranges[0], 'to' => $fileSize-1, 'size' => $fileSize );
269
				break;
270
			}
271
			elseif (is_numeric($ranges[1])) {
272
				// case: -x
273
				if ($ranges[1] > $fileSize) {
274
					$ranges[1] = $fileSize;
275
				}
276
				$rangeArray[$ind++] = array( 'from' => $fileSize-$ranges[1], 'to' => $fileSize-1, 'size' => $fileSize );
277
				break;
278
			}
279
		}
280
		return $rangeArray;
281
	}
282
283
	/**
284
	 * @param View $view
285
	 * @param string $name
286
	 * @param string $dir
287
	 * @param array $params ; 'head' boolean to only send header of the request ; 'range' http range header
288
	 */
289
	private static function getSingleFile($view, $dir, $name, $params) {
290
		$filename = $dir . '/' . $name;
291
		$file = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $file is dead and can be removed.
Loading history...
292
293
		try {
294
			$userFolder = \OC::$server->getRootFolder()->get(\OC\Files\Filesystem::getRoot());
295
			$file = $userFolder->get($filename);
0 ignored issues
show
Bug introduced by
The method get() does not exist on OCP\Files\Node. Did you maybe mean getId()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

295
			/** @scrutinizer ignore-call */ 
296
   $file = $userFolder->get($filename);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
296
			if(!$file instanceof \OC\Files\Node\File || !$file->isReadable()) {
297
				http_response_code(403);
298
				die('403 Forbidden');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
299
			}
300
			$fileSize = $file->getSize();
301
		} catch (\OCP\Files\NotPermittedException $e) {
302
			http_response_code(403);
303
			die('403 Forbidden');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
304
		} catch (\OCP\Files\InvalidPathException $e) {
305
			http_response_code(403);
306
			die('403 Forbidden');
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
307
		} catch (\OCP\Files\NotFoundException $e) {
308
			http_response_code(404);
309
			$tmpl = new OC_Template('', '404', 'guest');
310
			$tmpl->printPage();
311
			exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
312
		}
313
314
		OC_Util::obEnd();
315
		$view->lockFile($filename, ILockingProvider::LOCK_SHARED);
316
317
		$rangeArray = array();
318
319
		if (isset($params['range']) && substr($params['range'], 0, 6) === 'bytes=') {
320
			$rangeArray = self::parseHttpRangeHeader(substr($params['range'], 6), $fileSize);
321
		}
322
323
		self::sendHeaders($filename, $name, $rangeArray);
324
325
		if (isset($params['head']) && $params['head']) {
326
			return;
327
		}
328
329
		if (!empty($rangeArray)) {
330
			try {
331
			    if (count($rangeArray) == 1) {
332
				$view->readfilePart($filename, $rangeArray[0]['from'], $rangeArray[0]['to']);
333
			    }
334
			    else {
335
				// check if file is seekable (if not throw UnseekableException)
336
				// we have to check it before body contents
337
				$view->readfilePart($filename, $rangeArray[0]['size'], $rangeArray[0]['size']);
338
339
				$type = \OC::$server->getMimeTypeDetector()->getSecureMimeType(\OC\Files\Filesystem::getMimeType($filename));
340
341
				foreach ($rangeArray as $range) {
342
				    echo "\r\n--".self::getBoundary()."\r\n".
343
				         "Content-type: ".$type."\r\n".
344
				         "Content-range: bytes ".$range['from']."-".$range['to']."/".$range['size']."\r\n\r\n";
345
				    $view->readfilePart($filename, $range['from'], $range['to']);
346
				}
347
				echo "\r\n--".self::getBoundary()."--\r\n";
348
			    }
349
			} catch (\OCP\Files\UnseekableException $ex) {
350
			    // file is unseekable
351
			    header_remove('Accept-Ranges');
352
			    header_remove('Content-Range');
353
			    http_response_code(200);
354
			    self::sendHeaders($filename, $name, array());
355
			    $view->readfile($filename);
356
			}
357
		}
358
		else {
359
		    $view->readfile($filename);
360
		}
361
	}
362
363
	/**
364
	 * Returns the total (recursive) number of files and folders in the given
365
	 * FileInfos.
366
	 *
367
	 * @param \OCP\Files\FileInfo[] $fileInfos the FileInfos to count
368
	 * @return int the total number of files and folders
369
	 */
370
	private static function getNumberOfFiles($fileInfos) {
371
		$numberOfFiles = 0;
372
373
		$view = new View();
374
375
		while ($fileInfo = array_pop($fileInfos)) {
376
			$numberOfFiles++;
377
378
			if ($fileInfo->getType() === \OCP\Files\FileInfo::TYPE_FOLDER) {
379
				$fileInfos = array_merge($fileInfos, $view->getDirectoryContent($fileInfo->getPath()));
380
			}
381
		}
382
383
		return $numberOfFiles;
384
	}
385
386
	/**
387
	 * @param View $view
388
	 * @param string $dir
389
	 * @param string[]|string $files
390
	 */
391
	public static function lockFiles($view, $dir, $files) {
392
		if (!is_array($files)) {
393
			$file = $dir . '/' . $files;
394
			$files = [$file];
395
		}
396
		foreach ($files as $file) {
397
			$file = $dir . '/' . $file;
398
			$view->lockFile($file, ILockingProvider::LOCK_SHARED);
399
			if ($view->is_dir($file)) {
400
				$contents = $view->getDirectoryContent($file);
401
				$contents = array_map(function($fileInfo) use ($file) {
402
					/** @var \OCP\Files\FileInfo $fileInfo */
403
					return $file . '/' . $fileInfo->getName();
404
				}, $contents);
405
				self::lockFiles($view, $dir, $contents);
406
			}
407
		}
408
	}
409
410
	/**
411
	 * @param string $dir
412
	 * @param $files
413
	 * @param integer $getType
414
	 * @param View $view
415
	 * @param string $filename
416
	 */
417
	private static function unlockAllTheFiles($dir, $files, $getType, $view, $filename) {
418
		if ($getType === self::FILE) {
419
			$view->unlockFile($filename, ILockingProvider::LOCK_SHARED);
420
		}
421
		if ($getType === self::ZIP_FILES) {
422
			foreach ($files as $file) {
423
				$file = $dir . '/' . $file;
424
				$view->unlockFile($file, ILockingProvider::LOCK_SHARED);
425
			}
426
		}
427
		if ($getType === self::ZIP_DIR) {
428
			$file = $dir . '/' . $files;
429
			$view->unlockFile($file, ILockingProvider::LOCK_SHARED);
430
		}
431
	}
432
433
}
434