Completed
Push — master ( ca493a...4c38d1 )
by Morris
132:28 queued 111:40
created

DAV::rename()   B

Complexity

Conditions 3
Paths 18

Size

Total Lines 30
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 22
nc 18
nop 2
dl 0
loc 30
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bart Visscher <[email protected]>
6
 * @author Björn Schießle <[email protected]>
7
 * @author Carlos Cerrillo <[email protected]>
8
 * @author Jörn Friedrich Dreyer <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Michael Gapczynski <[email protected]>
11
 * @author Morris Jobke <[email protected]>
12
 * @author Philipp Kapfer <[email protected]>
13
 * @author Robin Appelman <[email protected]>
14
 * @author Thomas Müller <[email protected]>
15
 * @author Vincent Petry <[email protected]>
16
 * @author vkuimov "vkuimov@nextcloud"
17
 *
18
 * @license AGPL-3.0
19
 *
20
 * This code is free software: you can redistribute it and/or modify
21
 * it under the terms of the GNU Affero General Public License, version 3,
22
 * as published by the Free Software Foundation.
23
 *
24
 * This program is distributed in the hope that it will be useful,
25
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27
 * GNU Affero General Public License for more details.
28
 *
29
 * You should have received a copy of the GNU Affero General Public License, version 3,
30
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
31
 *
32
 */
33
34
namespace OC\Files\Storage;
35
36
use Exception;
37
use GuzzleHttp\Exception\RequestException;
38
use GuzzleHttp\Message\ResponseInterface;
39
use Icewind\Streams\CallbackWrapper;
40
use OC\Files\Filesystem;
41
use Icewind\Streams\IteratorDirectory;
42
use OC\MemCache\ArrayCache;
43
use OCP\AppFramework\Http;
44
use OCP\Constants;
45
use OCP\Files\FileInfo;
46
use OCP\Files\StorageInvalidException;
47
use OCP\Files\StorageNotAvailableException;
48
use OCP\Util;
49
use Sabre\DAV\Client;
50
use Sabre\DAV\Xml\Property\ResourceType;
51
use Sabre\HTTP\ClientException;
52
use Sabre\HTTP\ClientHttpException;
53
54
/**
55
 * Class DAV
56
 *
57
 * @package OC\Files\Storage
58
 */
59
class DAV extends Common {
60
	/** @var string */
61
	protected $password;
62
	/** @var string */
63
	protected $user;
64
	/** @var string */
65
	protected $authType;
66
	/** @var string */
67
	protected $host;
68
	/** @var bool */
69
	protected $secure;
70
	/** @var string */
71
	protected $root;
72
	/** @var string */
73
	protected $certPath;
74
	/** @var bool */
75
	protected $ready;
76
	/** @var Client */
77
	protected $client;
78
	/** @var ArrayCache */
79
	protected $statCache;
80
	/** @var \OCP\Http\Client\IClientService */
81
	protected $httpClientService;
82
83
	/**
84
	 * @param array $params
85
	 * @throws \Exception
86
	 */
87
	public function __construct($params) {
88
		$this->statCache = new ArrayCache();
89
		$this->httpClientService = \OC::$server->getHTTPClientService();
90
		if (isset($params['host']) && isset($params['user']) && isset($params['password'])) {
91
			$host = $params['host'];
92
			//remove leading http[s], will be generated in createBaseUri()
93
			if (substr($host, 0, 8) == "https://") $host = substr($host, 8);
94
			else if (substr($host, 0, 7) == "http://") $host = substr($host, 7);
95
			$this->host = $host;
96
			$this->user = $params['user'];
97
			$this->password = $params['password'];
98
			if (isset($params['authType'])) {
99
				$this->authType = $params['authType'];
100
			}
101
			if (isset($params['secure'])) {
102
				if (is_string($params['secure'])) {
103
					$this->secure = ($params['secure'] === 'true');
104
				} else {
105
					$this->secure = (bool)$params['secure'];
106
				}
107
			} else {
108
				$this->secure = false;
109
			}
110
			if ($this->secure === true) {
111
				// inject mock for testing
112
				$certManager = \OC::$server->getCertificateManager();
113
				if (is_null($certManager)) { //no user
114
					$certManager = \OC::$server->getCertificateManager(null);
115
				}
116
				$certPath = $certManager->getAbsoluteBundlePath();
117
				if (file_exists($certPath)) {
118
					$this->certPath = $certPath;
119
				}
120
			}
121
			$this->root = $params['root'] ?? '/';
122
			$this->root = '/' . ltrim($this->root, '/');
123
			$this->root = rtrim($this->root, '/') . '/';
124
		} else {
125
			throw new \Exception('Invalid webdav storage configuration');
126
		}
127
	}
128
129
	protected function init() {
130
		if ($this->ready) {
131
			return;
132
		}
133
		$this->ready = true;
134
135
		$settings = [
136
			'baseUri' => $this->createBaseUri(),
137
			'userName' => $this->user,
138
			'password' => $this->password,
139
		];
140
		if (isset($this->authType)) {
141
			$settings['authType'] = $this->authType;
142
		}
143
144
		$proxy = \OC::$server->getConfig()->getSystemValue('proxy', '');
145
		if ($proxy !== '') {
146
			$settings['proxy'] = $proxy;
147
		}
148
149
		$this->client = new Client($settings);
150
		$this->client->setThrowExceptions(true);
151
		if ($this->secure === true && $this->certPath) {
152
			$this->client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
153
		}
154
	}
155
156
	/**
157
	 * Clear the stat cache
158
	 */
159
	public function clearStatCache() {
160
		$this->statCache->clear();
161
	}
162
163
	/** {@inheritdoc} */
164
	public function getId() {
165
		return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root;
166
	}
167
168
	/** {@inheritdoc} */
169
	public function createBaseUri() {
170
		$baseUri = 'http';
171
		if ($this->secure) {
172
			$baseUri .= 's';
173
		}
174
		$baseUri .= '://' . $this->host . $this->root;
175
		return $baseUri;
176
	}
177
178
	/** {@inheritdoc} */
179
	public function mkdir($path) {
180
		$this->init();
181
		$path = $this->cleanPath($path);
182
		$result = $this->simpleResponse('MKCOL', $path, null, 201);
183
		if ($result) {
184
			$this->statCache->set($path, true);
185
		}
186
		return $result;
187
	}
188
189
	/** {@inheritdoc} */
190 View Code Duplication
	public function rmdir($path) {
191
		$this->init();
192
		$path = $this->cleanPath($path);
193
		// FIXME: some WebDAV impl return 403 when trying to DELETE
194
		// a non-empty folder
195
		$result = $this->simpleResponse('DELETE', $path . '/', null, 204);
196
		$this->statCache->clear($path . '/');
197
		$this->statCache->remove($path);
198
		return $result;
199
	}
200
201
	/** {@inheritdoc} */
202
	public function opendir($path) {
203
		$this->init();
204
		$path = $this->cleanPath($path);
205
		try {
206
			$response = $this->client->propFind(
207
				$this->encodePath($path),
208
				['{DAV:}href'],
209
				1
210
			);
211
			if ($response === false) {
212
				return false;
213
			}
214
			$content = [];
215
			$files = array_keys($response);
216
			array_shift($files); //the first entry is the current directory
217
218
			if (!$this->statCache->hasKey($path)) {
219
				$this->statCache->set($path, true);
220
			}
221
			foreach ($files as $file) {
222
				$file = urldecode($file);
223
				// do not store the real entry, we might not have all properties
224
				if (!$this->statCache->hasKey($path)) {
225
					$this->statCache->set($file, true);
226
				}
227
				$file = basename($file);
228
				$content[] = $file;
229
			}
230
			return IteratorDirectory::wrap($content);
231
		} catch (\Exception $e) {
232
			$this->convertException($e, $path);
233
		}
234
		return false;
235
	}
236
237
	/**
238
	 * Propfind call with cache handling.
239
	 *
240
	 * First checks if information is cached.
241
	 * If not, request it from the server then store to cache.
242
	 *
243
	 * @param string $path path to propfind
244
	 *
245
	 * @return array|boolean propfind response or false if the entry was not found
246
	 *
247
	 * @throws ClientHttpException
248
	 */
249
	protected function propfind($path) {
250
		$path = $this->cleanPath($path);
251
		$cachedResponse = $this->statCache->get($path);
252
		// we either don't know it, or we know it exists but need more details
253
		if (is_null($cachedResponse) || $cachedResponse === true) {
254
			$this->init();
255
			try {
256
				$response = $this->client->propFind(
257
					$this->encodePath($path),
258
					array(
259
						'{DAV:}getlastmodified',
260
						'{DAV:}getcontentlength',
261
						'{DAV:}getcontenttype',
262
						'{http://owncloud.org/ns}permissions',
263
						'{http://open-collaboration-services.org/ns}share-permissions',
264
						'{DAV:}resourcetype',
265
						'{DAV:}getetag',
266
					)
267
				);
268
				$this->statCache->set($path, $response);
269
			} catch (ClientHttpException $e) {
0 ignored issues
show
Bug introduced by
The class Sabre\HTTP\ClientHttpException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
270 View Code Duplication
				if ($e->getHttpStatus() === 404) {
271
					$this->statCache->clear($path . '/');
272
					$this->statCache->set($path, false);
273
					return false;
274
				}
275
				$this->convertException($e, $path);
276
			} catch (\Exception $e) {
277
				$this->convertException($e, $path);
278
			}
279
		} else {
280
			$response = $cachedResponse;
281
		}
282
		return $response;
283
	}
284
285
	/** {@inheritdoc} */
286
	public function filetype($path) {
287
		try {
288
			$response = $this->propfind($path);
289
			if ($response === false) {
290
				return false;
291
			}
292
			$responseType = [];
293
			if (isset($response["{DAV:}resourcetype"])) {
294
				/** @var ResourceType[] $response */
295
				$responseType = $response["{DAV:}resourcetype"]->getValue();
296
			}
297
			return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
298
		} catch (\Exception $e) {
299
			$this->convertException($e, $path);
300
		}
301
		return false;
302
	}
303
304
	/** {@inheritdoc} */
305
	public function file_exists($path) {
306
		try {
307
			$path = $this->cleanPath($path);
308
			$cachedState = $this->statCache->get($path);
309
			if ($cachedState === false) {
310
				// we know the file doesn't exist
311
				return false;
312
			} else if (!is_null($cachedState)) {
313
				return true;
314
			}
315
			// need to get from server
316
			return ($this->propfind($path) !== false);
317
		} catch (\Exception $e) {
318
			$this->convertException($e, $path);
319
		}
320
		return false;
321
	}
322
323
	/** {@inheritdoc} */
324 View Code Duplication
	public function unlink($path) {
325
		$this->init();
326
		$path = $this->cleanPath($path);
327
		$result = $this->simpleResponse('DELETE', $path, null, 204);
328
		$this->statCache->clear($path . '/');
329
		$this->statCache->remove($path);
330
		return $result;
331
	}
332
333
	/** {@inheritdoc} */
334
	public function fopen($path, $mode) {
335
		$this->init();
336
		$path = $this->cleanPath($path);
337
		switch ($mode) {
338
			case 'r':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
339
			case 'rb':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
340
				try {
341
					$response = $this->httpClientService
342
						->newClient()
343
						->get($this->createBaseUri() . $this->encodePath($path), [
344
							'auth' => [$this->user, $this->password],
345
							'stream' => true
346
						]);
347
				} catch (RequestException $e) {
0 ignored issues
show
Bug introduced by
The class GuzzleHttp\Exception\RequestException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
348
					if ($e->getResponse() instanceof ResponseInterface
0 ignored issues
show
Bug introduced by
The class GuzzleHttp\Message\ResponseInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
349
						&& $e->getResponse()->getStatusCode() === 404) {
350
						return false;
351
					} else {
352
						throw $e;
353
					}
354
				}
355
356
				if ($response->getStatusCode() !== Http::STATUS_OK) {
357
					if ($response->getStatusCode() === Http::STATUS_LOCKED) {
358
						throw new \OCP\Lock\LockedException($path);
359
					} else {
360
						Util::writeLog("webdav client", 'Guzzle get returned status code ' . $response->getStatusCode(), Util::ERROR);
361
					}
362
				}
363
364
				return $response->getBody();
0 ignored issues
show
Bug Compatibility introduced by
The expression $response->getBody(); of type string|resource adds the type string to the return on line 364 which is incompatible with the return type declared by the interface OCP\Files\Storage::fopen of type resource|false.
Loading history...
365
			case 'w':
366
			case 'wb':
367
			case 'a':
368
			case 'ab':
369
			case 'r+':
370
			case 'w+':
371
			case 'wb+':
372
			case 'a+':
373
			case 'x':
374
			case 'x+':
375
			case 'c':
376
			case 'c+':
377
				//emulate these
378
				$tempManager = \OC::$server->getTempManager();
379
				if (strrpos($path, '.') !== false) {
380
					$ext = substr($path, strrpos($path, '.'));
381
				} else {
382
					$ext = '';
383
				}
384
				if ($this->file_exists($path)) {
385
					if (!$this->isUpdatable($path)) {
386
						return false;
387
					}
388
					if ($mode === 'w' or $mode === 'w+') {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
389
						$tmpFile = $tempManager->getTemporaryFile($ext);
390
					} else {
391
						$tmpFile = $this->getCachedFile($path);
392
					}
393
				} else {
394
					if (!$this->isCreatable(dirname($path))) {
395
						return false;
396
					}
397
					$tmpFile = $tempManager->getTemporaryFile($ext);
398
				}
399
				$handle = fopen($tmpFile, $mode);
400
				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
401
					$this->writeBack($tmpFile, $path);
402
				});
403
		}
404
	}
405
406
	/**
407
	 * @param string $tmpFile
408
	 */
409
	public function writeBack($tmpFile, $path) {
410
		$this->uploadFile($tmpFile, $path);
411
		unlink($tmpFile);
412
	}
413
414
	/** {@inheritdoc} */
415
	public function free_space($path) {
416
		$this->init();
417
		$path = $this->cleanPath($path);
418
		try {
419
			// TODO: cacheable ?
420
			$response = $this->client->propfind($this->encodePath($path), ['{DAV:}quota-available-bytes']);
421
			if ($response === false) {
422
				return FileInfo::SPACE_UNKNOWN;
423
			}
424
			if (isset($response['{DAV:}quota-available-bytes'])) {
425
				return (int)$response['{DAV:}quota-available-bytes'];
426
			} else {
427
				return FileInfo::SPACE_UNKNOWN;
428
			}
429
		} catch (\Exception $e) {
430
			return FileInfo::SPACE_UNKNOWN;
431
		}
432
	}
433
434
	/** {@inheritdoc} */
435
	public function touch($path, $mtime = null) {
436
		$this->init();
437
		if (is_null($mtime)) {
438
			$mtime = time();
439
		}
440
		$path = $this->cleanPath($path);
441
442
		// if file exists, update the mtime, else create a new empty file
443
		if ($this->file_exists($path)) {
444
			try {
445
				$this->statCache->remove($path);
446
				$this->client->proppatch($this->encodePath($path), ['{DAV:}lastmodified' => $mtime]);
447
				// non-owncloud clients might not have accepted the property, need to recheck it
448
				$response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0);
449
				if ($response === false) {
450
					return false;
451
				}
452
				if (isset($response['{DAV:}getlastmodified'])) {
453
					$remoteMtime = strtotime($response['{DAV:}getlastmodified']);
454
					if ($remoteMtime !== $mtime) {
455
						// server has not accepted the mtime
456
						return false;
457
					}
458
				}
459
			} catch (ClientHttpException $e) {
0 ignored issues
show
Bug introduced by
The class Sabre\HTTP\ClientHttpException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
460
				if ($e->getHttpStatus() === 501) {
461
					return false;
462
				}
463
				$this->convertException($e, $path);
464
				return false;
465
			} catch (\Exception $e) {
466
				$this->convertException($e, $path);
467
				return false;
468
			}
469
		} else {
470
			$this->file_put_contents($path, '');
471
		}
472
		return true;
473
	}
474
475
	/**
476
	 * @param string $path
477
	 * @param string $data
478
	 * @return int
479
	 */
480
	public function file_put_contents($path, $data) {
481
		$path = $this->cleanPath($path);
482
		$result = parent::file_put_contents($path, $data);
483
		$this->statCache->remove($path);
484
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $result; (integer) is incompatible with the return type declared by the interface OCP\Files\Storage::file_put_contents of type boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
485
	}
486
487
	/**
488
	 * @param string $path
489
	 * @param string $target
490
	 */
491
	protected function uploadFile($path, $target) {
492
		$this->init();
493
494
		// invalidate
495
		$target = $this->cleanPath($target);
496
		$this->statCache->remove($target);
497
		$source = fopen($path, 'r');
498
499
		$this->httpClientService
500
			->newClient()
501
			->put($this->createBaseUri() . $this->encodePath($target), [
502
				'body' => $source,
503
				'auth' => [$this->user, $this->password]
504
			]);
505
506
		$this->removeCachedFile($target);
507
	}
508
509
	/** {@inheritdoc} */
510
	public function rename($path1, $path2) {
511
		$this->init();
512
		$path1 = $this->cleanPath($path1);
513
		$path2 = $this->cleanPath($path2);
514
		try {
515
			// overwrite directory ?
516
			if ($this->is_dir($path2)) {
517
				// needs trailing slash in destination
518
				$path2 = rtrim($path2, '/') . '/';
519
			}
520
			$this->client->request(
521
				'MOVE',
522
				$this->encodePath($path1),
523
				null,
524
				[
525
					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
526
				]
527
			);
528
			$this->statCache->clear($path1 . '/');
529
			$this->statCache->clear($path2 . '/');
530
			$this->statCache->set($path1, false);
531
			$this->statCache->set($path2, true);
532
			$this->removeCachedFile($path1);
533
			$this->removeCachedFile($path2);
534
			return true;
535
		} catch (\Exception $e) {
536
			$this->convertException($e);
537
		}
538
		return false;
539
	}
540
541
	/** {@inheritdoc} */
542
	public function copy($path1, $path2) {
543
		$this->init();
544
		$path1 = $this->cleanPath($path1);
545
		$path2 = $this->cleanPath($path2);
546
		try {
547
			// overwrite directory ?
548
			if ($this->is_dir($path2)) {
549
				// needs trailing slash in destination
550
				$path2 = rtrim($path2, '/') . '/';
551
			}
552
			$this->client->request(
553
				'COPY',
554
				$this->encodePath($path1),
555
				null,
556
				[
557
					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
558
				]
559
			);
560
			$this->statCache->clear($path2 . '/');
561
			$this->statCache->set($path2, true);
562
			$this->removeCachedFile($path2);
563
			return true;
564
		} catch (\Exception $e) {
565
			$this->convertException($e);
566
		}
567
		return false;
568
	}
569
570
	/** {@inheritdoc} */
571
	public function stat($path) {
572
		try {
573
			$response = $this->propfind($path);
574
			if (!$response) {
575
				return false;
576
			}
577
			return [
578
				'mtime' => strtotime($response['{DAV:}getlastmodified']),
579
				'size' => (int)isset($response['{DAV:}getcontentlength']) ? $response['{DAV:}getcontentlength'] : 0,
580
			];
581
		} catch (\Exception $e) {
582
			$this->convertException($e, $path);
583
		}
584
		return array();
585
	}
586
587
	/** {@inheritdoc} */
588
	public function getMimeType($path) {
589
		$remoteMimetype = $this->getMimeTypeFromRemote($path);
590
		if ($remoteMimetype === 'application/octet-stream') {
591
			return \OC::$server->getMimeTypeDetector()->detectPath($path);
592
		} else {
593
			return $remoteMimetype;
594
		}
595
	}
596
597
	public function getMimeTypeFromRemote($path) {
598
		try {
599
			$response = $this->propfind($path);
600
			if ($response === false) {
601
				return false;
602
			}
603
			$responseType = [];
604
			if (isset($response["{DAV:}resourcetype"])) {
605
				/** @var ResourceType[] $response */
606
				$responseType = $response["{DAV:}resourcetype"]->getValue();
607
			}
608
			$type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
609
			if ($type == 'dir') {
610
				return 'httpd/unix-directory';
611
			} elseif (isset($response['{DAV:}getcontenttype'])) {
612
				return $response['{DAV:}getcontenttype'];
613
			} else {
614
				return 'application/octet-stream';
615
			}
616
		} catch (\Exception $e) {
617
			return false;
618
		}
619
	}
620
621
	/**
622
	 * @param string $path
623
	 * @return string
624
	 */
625
	public function cleanPath($path) {
626
		if ($path === '') {
627
			return $path;
628
		}
629
		$path = Filesystem::normalizePath($path);
630
		// remove leading slash
631
		return substr($path, 1);
632
	}
633
634
	/**
635
	 * URL encodes the given path but keeps the slashes
636
	 *
637
	 * @param string $path to encode
638
	 * @return string encoded path
639
	 */
640
	protected function encodePath($path) {
641
		// slashes need to stay
642
		return str_replace('%2F', '/', rawurlencode($path));
643
	}
644
645
	/**
646
	 * @param string $method
647
	 * @param string $path
648
	 * @param string|resource|null $body
649
	 * @param int $expected
650
	 * @return bool
651
	 * @throws StorageInvalidException
652
	 * @throws StorageNotAvailableException
653
	 */
654
	protected function simpleResponse($method, $path, $body, $expected) {
655
		$path = $this->cleanPath($path);
656
		try {
657
			$response = $this->client->request($method, $this->encodePath($path), $body);
658
			return $response['statusCode'] == $expected;
659
		} catch (ClientHttpException $e) {
0 ignored issues
show
Bug introduced by
The class Sabre\HTTP\ClientHttpException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
660 View Code Duplication
			if ($e->getHttpStatus() === 404 && $method === 'DELETE') {
661
				$this->statCache->clear($path . '/');
662
				$this->statCache->set($path, false);
663
				return false;
664
			}
665
666
			$this->convertException($e, $path);
667
		} catch (\Exception $e) {
668
			$this->convertException($e, $path);
669
		}
670
		return false;
671
	}
672
673
	/**
674
	 * check if curl is installed
675
	 */
676
	public static function checkDependencies() {
677
		return true;
678
	}
679
680
	/** {@inheritdoc} */
681
	public function isUpdatable($path) {
682
		return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
683
	}
684
685
	/** {@inheritdoc} */
686
	public function isCreatable($path) {
687
		return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
688
	}
689
690
	/** {@inheritdoc} */
691
	public function isSharable($path) {
692
		return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
693
	}
694
695
	/** {@inheritdoc} */
696
	public function isDeletable($path) {
697
		return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
698
	}
699
700
	/** {@inheritdoc} */
701
	public function getPermissions($path) {
702
		$this->init();
703
		$path = $this->cleanPath($path);
704
		$response = $this->propfind($path);
705
		if ($response === false) {
706
			return 0;
707
		}
708
		if (isset($response['{http://owncloud.org/ns}permissions'])) {
709
			return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
710
		} else if ($this->is_dir($path)) {
711
			return Constants::PERMISSION_ALL;
712
		} else if ($this->file_exists($path)) {
713
			return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
714
		} else {
715
			return 0;
716
		}
717
	}
718
719
	/** {@inheritdoc} */
720
	public function getETag($path) {
721
		$this->init();
722
		$path = $this->cleanPath($path);
723
		$response = $this->propfind($path);
724
		if ($response === false) {
725
			return null;
726
		}
727
		if (isset($response['{DAV:}getetag'])) {
728
			return trim($response['{DAV:}getetag'], '"');
729
		}
730
		return parent::getEtag($path);
731
	}
732
733
	/**
734
	 * @param string $permissionsString
735
	 * @return int
736
	 */
737
	protected function parsePermissions($permissionsString) {
738
		$permissions = Constants::PERMISSION_READ;
739
		if (strpos($permissionsString, 'R') !== false) {
740
			$permissions |= Constants::PERMISSION_SHARE;
741
		}
742
		if (strpos($permissionsString, 'D') !== false) {
743
			$permissions |= Constants::PERMISSION_DELETE;
744
		}
745
		if (strpos($permissionsString, 'W') !== false) {
746
			$permissions |= Constants::PERMISSION_UPDATE;
747
		}
748
		if (strpos($permissionsString, 'CK') !== false) {
749
			$permissions |= Constants::PERMISSION_CREATE;
750
			$permissions |= Constants::PERMISSION_UPDATE;
751
		}
752
		return $permissions;
753
	}
754
755
	/**
756
	 * check if a file or folder has been updated since $time
757
	 *
758
	 * @param string $path
759
	 * @param int $time
760
	 * @throws \OCP\Files\StorageNotAvailableException
761
	 * @return bool
762
	 */
763
	public function hasUpdated($path, $time) {
764
		$this->init();
765
		$path = $this->cleanPath($path);
766
		try {
767
			// force refresh for $path
768
			$this->statCache->remove($path);
769
			$response = $this->propfind($path);
770 View Code Duplication
			if ($response === false) {
771
				if ($path === '') {
772
					// if root is gone it means the storage is not available
773
					throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
774
				}
775
				return false;
776
			}
777
			if (isset($response['{DAV:}getetag'])) {
778
				$cachedData = $this->getCache()->get($path);
779
				$etag = null;
780
				if (isset($response['{DAV:}getetag'])) {
781
					$etag = trim($response['{DAV:}getetag'], '"');
782
				}
783
				if (!empty($etag) && $cachedData['etag'] !== $etag) {
784
					return true;
785
				} else if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
786
					$sharePermissions = (int)$response['{http://open-collaboration-services.org/ns}share-permissions'];
787
					return $sharePermissions !== $cachedData['permissions'];
788
				} else if (isset($response['{http://owncloud.org/ns}permissions'])) {
789
					$permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
790
					return $permissions !== $cachedData['permissions'];
791
				} else {
792
					return false;
793
				}
794
			} else {
795
				$remoteMtime = strtotime($response['{DAV:}getlastmodified']);
796
				return $remoteMtime > $time;
797
			}
798
		} catch (ClientHttpException $e) {
0 ignored issues
show
Bug introduced by
The class Sabre\HTTP\ClientHttpException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
799 View Code Duplication
			if ($e->getHttpStatus() === 405) {
800
				if ($path === '') {
801
					// if root is gone it means the storage is not available
802
					throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
803
				}
804
				return false;
805
			}
806
			$this->convertException($e, $path);
807
			return false;
808
		} catch (\Exception $e) {
809
			$this->convertException($e, $path);
810
			return false;
811
		}
812
	}
813
814
	/**
815
	 * Interpret the given exception and decide whether it is due to an
816
	 * unavailable storage, invalid storage or other.
817
	 * This will either throw StorageInvalidException, StorageNotAvailableException
818
	 * or do nothing.
819
	 *
820
	 * @param Exception $e sabre exception
821
	 * @param string $path optional path from the operation
822
	 *
823
	 * @throws StorageInvalidException if the storage is invalid, for example
824
	 * when the authentication expired or is invalid
825
	 * @throws StorageNotAvailableException if the storage is not available,
826
	 * which might be temporary
827
	 */
828
	protected function convertException(Exception $e, $path = '') {
829
		\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
830
		if ($e instanceof ClientHttpException) {
0 ignored issues
show
Bug introduced by
The class Sabre\HTTP\ClientHttpException does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
831
			if ($e->getHttpStatus() === Http::STATUS_LOCKED) {
832
				throw new \OCP\Lock\LockedException($path);
833
			}
834
			if ($e->getHttpStatus() === Http::STATUS_UNAUTHORIZED) {
835
				// either password was changed or was invalid all along
836
				throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
837
			} else if ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED) {
838
				// ignore exception for MethodNotAllowed, false will be returned
839
				return;
840
			}
841
			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
842
		} else if ($e instanceof ClientException) {
0 ignored issues
show
Bug introduced by
The class Sabre\HTTP\ClientException does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
843
			// connection timeout or refused, server could be temporarily down
844
			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
845
		} else if ($e instanceof \InvalidArgumentException) {
846
			// parse error because the server returned HTML instead of XML,
847
			// possibly temporarily down
848
			throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
849
		} else if (($e instanceof StorageNotAvailableException) || ($e instanceof StorageInvalidException)) {
850
			// rethrow
851
			throw $e;
852
		}
853
854
		// TODO: only log for now, but in the future need to wrap/rethrow exception
855
	}
856
}
857
858