Completed
Push — master ( b5a2f8...e951b9 )
by Thomas
19:24 queued 07:30
created

OC_Files::getSingleFile()   C

Complexity

Conditions 11
Paths 26

Size

Total Lines 59
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 41
nc 26
nop 4
dl 0
loc 59
rs 6.3545
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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) 2017, 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
55
	private static $multipartBoundary = '';
56
57
	/**
58
	 * @return string
59
	 */
60
	private static function getBoundary() {
61
		if (empty(self::$multipartBoundary)) {
62
			self::$multipartBoundary = md5(mt_rand());
63
		}
64
		return self::$multipartBoundary;
65
	}
66
67
	/**
68
	 * @param string $filename
69
	 * @param string $name
70
	 * @param array $rangeArray ('from'=>int,'to'=>int), ...
71
	 */
72
	private static function sendHeaders($filename, $name, array $rangeArray) {
73
		OC_Response::setContentDispositionHeader($name, 'attachment');
74
		header('Content-Transfer-Encoding: binary', true);
75
		OC_Response::disableCaching();
76
		$fileSize = \OC\Files\Filesystem::filesize($filename);
77
		$type = \OC::$server->getMimeTypeDetector()->getSecureMimeType(\OC\Files\Filesystem::getMimeType($filename));
78
		if ($fileSize > -1) {
79
			if (!empty($rangeArray)) {
80
			    header('HTTP/1.1 206 Partial Content', true);
81
			    header('Accept-Ranges: bytes', true);
82
			    if (count($rangeArray) > 1) {
83
				$type = 'multipart/byteranges; boundary='.self::getBoundary();
84
				// no Content-Length header here
85
			    }
86
			    else {
87
				header(sprintf('Content-Range: bytes %d-%d/%d', $rangeArray[0]['from'], $rangeArray[0]['to'], $fileSize), true);
88
				OC_Response::setContentLengthHeader($rangeArray[0]['to'] - $rangeArray[0]['from'] + 1);
89
			    }
90
			}
91
			else {
92
			    OC_Response::setContentLengthHeader($fileSize);
93
			}
94
		}
95
		header('Content-Type: '.$type, true);
96
	}
97
98
	/**
99
	 * return the content of a file or return a zip file containing multiple files
100
	 *
101
	 * @param string $dir
102
	 * @param string $files ; separated list of files to download
103
	 * @param array $params ; 'head' boolean to only send header of the request ; 'range' http range header
104
	 */
105
	public static function get($dir, $files, $params = null) {
106
107
		$view = \OC\Files\Filesystem::getView();
108
		$getType = self::FILE;
109
		$filename = $dir;
110
		try {
111
112
			if (is_array($files) && count($files) === 1) {
113
				$files = $files[0];
114
			}
115
116
			if (!is_array($files)) {
117
				$filename = $dir . '/' . $files;
118
				if (!$view->is_dir($filename)) {
119
					self::getSingleFile($view, $dir, $files, is_null($params) ? [] : $params);
120
					return;
121
				}
122
			}
123
124
			$name = 'download';
125
			if (is_array($files)) {
126
				$getType = self::ZIP_FILES;
127
				$basename = basename($dir);
128
				if ($basename) {
129
					$name = $basename;
130
				}
131
132
				$filename = $dir . '/' . $name;
133
			} else {
134
				$filename = $dir . '/' . $files;
135
				$getType = self::ZIP_DIR;
136
				// downloading root ?
137
				if ($files !== '') {
138
					$name = $files;
139
				}
140
			}
141
142
			$streamer = new Streamer();
143
			OC_Util::obEnd();
144
145
			self::lockFiles($view, $dir, $files);
146
147
			$streamer->sendHeaders($name);
148
			$executionTime = intval(OC::$server->getIniWrapper()->getNumeric('max_execution_time'));
149
			set_time_limit(0);
150
			ignore_user_abort(true);
151
			if ($getType === self::ZIP_FILES) {
152
				foreach ($files as $file) {
153
					$file = $dir . '/' . $file;
154 View Code Duplication
					if (\OC\Files\Filesystem::is_file($file)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
155
						$fileSize = \OC\Files\Filesystem::filesize($file);
156
						$fh = \OC\Files\Filesystem::fopen($file, 'r');
157
						$streamer->addFileFromStream($fh, basename($file), $fileSize);
0 ignored issues
show
Documentation introduced by
$fh is of type resource, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
158
						fclose($fh);
159
					} elseif (\OC\Files\Filesystem::is_dir($file)) {
160
						$streamer->addDirRecursive($file);
161
					}
162
				}
163
			} elseif ($getType === self::ZIP_DIR) {
164
				$file = $dir . '/' . $files;
165
				$streamer->addDirRecursive($file);
166
			}
167
			$streamer->finalize();
168
			set_time_limit($executionTime);
169
			self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
170
		} catch (\OCP\Lock\LockedException $ex) {
171
			self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
172
			OC::$server->getLogger()->logException($ex);
173
			$l = \OC::$server->getL10N('core');
174
			$hint = method_exists($ex, 'getHint') ? $ex->getHint() : '';
175
			\OC_Template::printErrorPage($l->t('File is currently busy, please try again later'), $hint);
176
		} catch (\OCP\Files\ForbiddenException $ex) {
177
			self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
178
			OC::$server->getLogger()->logException($ex);
179
			$l = \OC::$server->getL10N('core');
180
			\OC_Template::printErrorPage($l->t('Can\'t read file'), $ex->getMessage(), 403);
181
		} catch (\Exception $ex) {
182
			self::unlockAllTheFiles($dir, $files, $getType, $view, $filename);
183
			OC::$server->getLogger()->logException($ex);
184
			$l = \OC::$server->getL10N('core');
185
			$hint = method_exists($ex, 'getHint') ? $ex->getHint() : '';
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Exception as the method getHint() does only exist in the following sub-classes of Exception: OCP\Encryption\Exception...ericEncryptionException, OCP\Files\External\Insuf...aningfulAnswerException, OCP\Files\StorageAuthException, OCP\Files\StorageBadConfigException, OCP\Files\StorageConnectionException, OCP\Files\StorageNotAvailableException, OCP\Files\StorageTimeoutException, OCP\Share\Exceptions\GenericShareException, OCP\Share\Exceptions\IllegalIDChangeException, OCP\Share\Exceptions\ShareNotFound, OC\DatabaseSetupException, OC\Encryption\Exceptions\DecryptionFailedException, OC\Encryption\Exceptions...EncryptionDataException, OC\Encryption\Exceptions\EncryptionFailedException, OC\Encryption\Exceptions...eaderKeyExistsException, OC\Encryption\Exceptions...nHeaderToLargeException, OC\Encryption\Exceptions...eAlreadyExistsException, OC\Encryption\Exceptions...eDoesNotExistsException, OC\Encryption\Exceptions\UnknownCipherException, OC\HintException. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
186
			\OC_Template::printErrorPage($l->t('Can\'t read file'), $hint);
187
		}
188
	}
189
190
	/**
191
	 * @param string $rangeHeaderPos
192
	 * @param int $fileSize
193
	 * @return array $rangeArray ('from'=>int,'to'=>int), ...
194
	 */
195
	private static function parseHttpRangeHeader($rangeHeaderPos, $fileSize) {
196
		$rArray=explode(',', $rangeHeaderPos);
197
		$minOffset = 0;
198
		$ind = 0;
199
200
		$rangeArray = [];
201
202
		foreach ($rArray as $value) {
203
			$ranges = explode('-', $value);
204
			if (is_numeric($ranges[0])) {
205
				if ($ranges[0] < $minOffset) { // case: bytes=500-700,601-999
206
					$ranges[0] = $minOffset;
207
				}
208
				if ($ind > 0 && $rangeArray[$ind-1]['to']+1 == $ranges[0]) { // case: bytes=500-600,601-999
209
					$ind--;
210
					$ranges[0] = $rangeArray[$ind]['from'];
211
				}
212
			}
213
214
			if (is_numeric($ranges[0]) && is_numeric($ranges[1]) && $ranges[0] < $fileSize && $ranges[0] <= $ranges[1]) {
215
				// case: x-x
216
				if ($ranges[1] >= $fileSize) {
217
					$ranges[1] = $fileSize-1;
218
				}
219
				$rangeArray[$ind++] = ['from' => $ranges[0], 'to' => $ranges[1], 'size' => $fileSize];
220
				$minOffset = $ranges[1] + 1;
221
				if ($minOffset >= $fileSize) {
222
					break;
223
				}
224
			}
225
			elseif (is_numeric($ranges[0]) && $ranges[0] < $fileSize) {
226
				// case: x-
227
				$rangeArray[$ind++] = ['from' => $ranges[0], 'to' => $fileSize-1, 'size' => $fileSize];
228
				break;
229
			}
230
			elseif (is_numeric($ranges[1])) {
231
				// case: -x
232
				if ($ranges[1] > $fileSize) {
233
					$ranges[1] = $fileSize;
234
				}
235
				$rangeArray[$ind++] = ['from' => $fileSize-$ranges[1], 'to' => $fileSize-1, 'size' => $fileSize];
236
				break;
237
			}
238
		}
239
		return $rangeArray;
240
	}
241
242
	/**
243
	 * @param View $view
244
	 * @param string $name
245
	 * @param string $dir
246
	 * @param array $params ; 'head' boolean to only send header of the request ; 'range' http range header
247
	 */
248
	private static function getSingleFile($view, $dir, $name, $params) {
249
		$filename = $dir . '/' . $name;
250
		OC_Util::obEnd();
251
		$view->lockFile($filename, ILockingProvider::LOCK_SHARED);
252
		
253
		$rangeArray = [];
254
255
		if (isset($params['range']) && substr($params['range'], 0, 6) === 'bytes=') {
256
			$rangeArray = self::parseHttpRangeHeader(substr($params['range'], 6), 
257
								 \OC\Files\Filesystem::filesize($filename));
258
		}
259
		
260
		if (\OC\Files\Filesystem::isReadable($filename)) {
261
			self::sendHeaders($filename, $name, $rangeArray);
262
		} elseif (!\OC\Files\Filesystem::file_exists($filename)) {
263
			header("HTTP/1.1 404 Not Found");
264
			$tmpl = new OC_Template('', '404', 'guest');
265
			$tmpl->printPage();
266
			exit();
267
		} else {
268
			header("HTTP/1.1 403 Forbidden");
269
			die('403 Forbidden');
270
		}
271
		if (isset($params['head']) && $params['head']) {
272
			return;
273
		}
274
		if (!empty($rangeArray)) {
275
			try {
276
			    if (count($rangeArray) == 1) {
277
				$view->readfilePart($filename, $rangeArray[0]['from'], $rangeArray[0]['to']);
278
			    }
279
			    else {
280
				// check if file is seekable (if not throw UnseekableException)
281
				// we have to check it before body contents
282
				$view->readfilePart($filename, $rangeArray[0]['size'], $rangeArray[0]['size']);
283
284
				$type = \OC::$server->getMimeTypeDetector()->getSecureMimeType(\OC\Files\Filesystem::getMimeType($filename));
285
286
				foreach ($rangeArray as $range) {
287
				    echo "\r\n--".self::getBoundary()."\r\n".
288
				         "Content-type: ".$type."\r\n".
289
				         "Content-range: bytes ".$range['from']."-".$range['to']."/".$range['size']."\r\n\r\n";
290
				    $view->readfilePart($filename, $range['from'], $range['to']);
291
				}
292
				echo "\r\n--".self::getBoundary()."--\r\n";
293
			    }
294
			} catch (\OCP\Files\UnseekableException $ex) {
295
			    // file is unseekable
296
			    header_remove('Accept-Ranges');
297
			    header_remove('Content-Range');
298
			    header("HTTP/1.1 200 OK");
299
			    self::sendHeaders($filename, $name, []);
300
			    $view->readfile($filename);
301
			}
302
		}
303
		else {
304
		    $view->readfile($filename);
305
		}
306
	}
307
308
	/**
309
	 * @param View $view
310
	 * @param string $dir
311
	 * @param string[]|string $files
312
	 */
313
	public static function lockFiles($view, $dir, $files) {
314
		if (!is_array($files)) {
315
			$file = $dir . '/' . $files;
316
			$files = [$file];
317
		}
318
		foreach ($files as $file) {
319
			$file = $dir . '/' . $file;
320
			$view->lockFile($file, ILockingProvider::LOCK_SHARED);
321
			if ($view->is_dir($file)) {
322
				$contents = $view->getDirectoryContent($file);
323
				$contents = array_map(function($fileInfo) use ($file) {
324
					/** @var \OCP\Files\FileInfo $fileInfo */
325
					return $file . '/' . $fileInfo->getName();
326
				}, $contents);
327
				self::lockFiles($view, $dir, $contents);
328
			}
329
		}
330
	}
331
332
	/**
333
	 * @param string $dir
334
	 * @param $files
335
	 * @param integer $getType
336
	 * @param View $view
337
	 * @param string $filename
338
	 */
339
	private static function unlockAllTheFiles($dir, $files, $getType, $view, $filename) {
340
		if ($getType === self::FILE) {
341
			$view->unlockFile($filename, ILockingProvider::LOCK_SHARED);
342
		}
343 View Code Duplication
		if ($getType === self::ZIP_FILES) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
344
			foreach ($files as $file) {
345
				$file = $dir . '/' . $file;
346
				$view->unlockFile($file, ILockingProvider::LOCK_SHARED);
347
			}
348
		}
349 View Code Duplication
		if ($getType === self::ZIP_DIR) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
350
			$file = $dir . '/' . $files;
351
			$view->unlockFile($file, ILockingProvider::LOCK_SHARED);
352
		}
353
	}
354
355
}
356