OC_Files::get()   F
last analyzed

Complexity

Conditions 22
Paths 5682

Size

Total Lines 94

Duplication

Lines 8
Ratio 8.51 %

Importance

Changes 0
Metric Value
cc 22
nc 5682
nop 3
dl 8
loc 94
rs 0
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) 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)) {
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...
157
						$fileSize = \OC\Files\Filesystem::filesize($file);
158
						$fh = \OC\Files\Filesystem::fopen($file, 'r');
159
						$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...
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() : '';
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, OCP\Share\Exceptions\TransferSharesException, 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...
190
			if ($event->hasArgument('message')) {
191
				$hint .= ' ' . $event->getArgument('message');
0 ignored issues
show
Bug introduced by
The variable $event does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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) {
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...
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) {
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...
353
			$file = $dir . '/' . $files;
354
			$view->unlockFile($file, ILockingProvider::LOCK_SHARED);
355
		}
356
	}
357
}
358