Completed
Pull Request — master (#32044)
by Thomas
19:39
created

DAV::isSharable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Bart Visscher <[email protected]>
4
 * @author Björn Schießle <[email protected]>
5
 * @author Carlos Cerrillo <[email protected]>
6
 * @author Felix Moeller <[email protected]>
7
 * @author Jörn Friedrich Dreyer <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Michael Gapczynski <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Philipp Kapfer <[email protected]>
12
 * @author Robin Appelman <[email protected]>
13
 * @author Thomas Müller <[email protected]>
14
 * @author Vincent Petry <[email protected]>
15
 *
16
 * @copyright Copyright (c) 2018, ownCloud GmbH
17
 * @license AGPL-3.0
18
 *
19
 * This code is free software: you can redistribute it and/or modify
20
 * it under the terms of the GNU Affero General Public License, version 3,
21
 * as published by the Free Software Foundation.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 * GNU Affero General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU Affero General Public License, version 3,
29
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
30
 *
31
 */
32
33
namespace OC\Files\Storage;
34
35
use Exception;
36
use GuzzleHttp\Exception\RequestException;
37
use GuzzleHttp\Message\ResponseInterface;
38
use function GuzzleHttp\Psr7\stream_for;
39
use OC\Files\Filesystem;
40
use OC\Files\Stream\Close;
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 Psr\Http\Message\StreamInterface;
50
use Sabre\DAV\Xml\Property\ResourceType;
51
use Sabre\HTTP\Client;
52
use Sabre\HTTP\ClientHttpException;
53
use Sabre\DAV\Exception\InsufficientStorage;
54
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
55
56
/**
57
 * Class DAV
58
 *
59
 * @package OC\Files\Storage
60
 */
61
class DAV extends Common {
62
	/** @var string */
63
	protected $password;
64
	/** @var string */
65
	protected $user;
66
	/** @var string */
67
	protected $authType;
68
	/** @var string */
69
	protected $host;
70
	/** @var bool */
71
	protected $secure;
72
	/** @var string */
73
	protected $root;
74
	/** @var string */
75
	protected $certPath;
76
	/** @var bool */
77
	protected $ready;
78
	/** @var Client */
79
	private $client;
80
	/** @var ArrayCache */
81
	private $statCache;
82
	/** @var array */
83
	private static $tempFiles = [];
84
	/** @var \OCP\Http\Client\IClientService */
85
	private $httpClientService;
86
87
	/** @var \OCP\Http\Client\IWebDavClientService */
88
	private $webDavClientService;
89
90
	/**
91
	 * @param array $params
92
	 * @throws \Exception
93
	 */
94
	public function __construct($params) {
95
		$this->statCache = new ArrayCache();
96
		$this->httpClientService = \OC::$server->getHTTPClientService();
97
		$this->webDavClientService = \OC::$server->getWebDavClientService();
98
		if (isset($params['host'], $params['user'], $params['password'])) {
99
			$host = $params['host'];
100
			//remove leading http[s], will be generated in createBaseUri()
101
			if (\substr($host, 0, 8) == "https://") {
102
				$host = \substr($host, 8);
103
			} elseif (\substr($host, 0, 7) == "http://") {
104
				$host = \substr($host, 7);
105
			}
106
			$this->host = $host;
107
			$this->user = $params['user'];
108
			$this->password = $params['password'];
109
			if (isset($params['authType'])) {
110
				$this->authType = $params['authType'];
111
			}
112
			if (isset($params['secure'])) {
113
				if (\is_string($params['secure'])) {
114
					$this->secure = ($params['secure'] === 'true');
115
				} else {
116
					$this->secure = (bool)$params['secure'];
117
				}
118
			} else {
119
				$this->secure = false;
120
			}
121
			$this->root = isset($params['root']) ? $params['root'] : '/';
122 View Code Duplication
			if (!$this->root || $this->root[0] != '/') {
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...
123
				$this->root = '/' . $this->root;
124
			}
125
			if (\substr($this->root, -1, 1) != '/') {
126
				$this->root .= '/';
127
			}
128
		} else {
129
			throw new \InvalidArgumentException('Invalid webdav storage configuration');
130
		}
131
	}
132
133
	protected function init() {
134
		if ($this->ready) {
135
			return;
136
		}
137
		$this->ready = true;
138
139
		$settings = [
140
			'baseUri' => $this->createBaseUri(),
141
			'userName' => $this->user,
142
			'password' => $this->password
143
		];
144
		if (isset($this->authType)) {
145
			$settings['authType'] = $this->authType;
146
		}
147
148
		$this->client = $this->webDavClientService->newClient($settings);
149
	}
150
151
	/**
152
	 * Clear the stat cache
153
	 */
154
	public function clearStatCache() {
155
		$this->statCache->clear();
156
	}
157
158
	/** {@inheritdoc} */
159
	public function getId() {
160
		return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root;
161
	}
162
163
	/** {@inheritdoc} */
164
	public function createBaseUri() {
165
		$baseUri = 'http';
166
		if ($this->secure) {
167
			$baseUri .= 's';
168
		}
169
		$baseUri .= '://' . $this->host . $this->root;
170
		return $baseUri;
171
	}
172
173
	/** {@inheritdoc} */
174
	public function mkdir($path) {
175
		$this->init();
176
		$path = $this->cleanPath($path);
177
		$result = $this->simpleResponse('MKCOL', $path, null, 201);
178
		if ($result) {
179
			$this->statCache->set($path, true);
180
		}
181
		return $result;
182
	}
183
184
	/** {@inheritdoc} */
185 View Code Duplication
	public function rmdir($path) {
186
		$this->init();
187
		$path = $this->cleanPath($path);
188
		// FIXME: some WebDAV impl return 403 when trying to DELETE
189
		// a non-empty folder
190
		$result = $this->simpleResponse('DELETE', $path . '/', null, 204);
191
		$this->statCache->clear($path . '/');
192
		$this->statCache->remove($path);
193
		return $result;
194
	}
195
196
	/** {@inheritdoc} */
197
	public function opendir($path) {
198
		$this->init();
199
		$path = $this->cleanPath($path);
200
		try {
201
			$response = $this->client->propfind(
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Sabre\HTTP\Client as the method propfind() does only exist in the following sub-classes of Sabre\HTTP\Client: Sabre\DAV\Client, Sabre\DAV\ClientMock. 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...
202
				$this->encodePath($path),
203
				[],
204
				1
205
			);
206
			if ($response === false) {
207
				return false;
208
			}
209
			$content = [];
210
			$files = \array_keys($response);
211
			\array_shift($files); //the first entry is the current directory
212
213
			if (!$this->statCache->hasKey($path)) {
214
				$this->statCache->set($path, true);
215
			}
216
			foreach ($files as $file) {
217
				$file = \urldecode($file);
218
				// do not store the real entry, we might not have all properties
219
				if (!$this->statCache->hasKey($path)) {
220
					$this->statCache->set($file, true);
221
				}
222
				$file = \basename($file);
223
				$content[] = $file;
224
			}
225
			return IteratorDirectory::wrap($content);
226
		} catch (ClientHttpException $e) {
227 View Code Duplication
			if ($e->getHttpStatus() === Http::STATUS_NOT_FOUND) {
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...
228
				$this->statCache->clear($path . '/');
229
				$this->statCache->set($path, false);
230
				return false;
231
			}
232
			$this->convertException($e, $path);
233
		} catch (\Exception $e) {
234
			$this->convertException($e, $path);
235
		}
236
		return false;
237
	}
238
239
	/**
240
	 * Propfind call with cache handling.
241
	 *
242
	 * First checks if information is cached.
243
	 * If not, request it from the server then store to cache.
244
	 *
245
	 * @param string $path path to propfind
246
	 *
247
	 * @return array|boolean propfind response or false if the entry was not found
248
	 *
249
	 * @throws ClientHttpException
250
	 */
251
	protected function propfind($path) {
252
		$path = $this->cleanPath($path);
253
		$cachedResponse = $this->statCache->get($path);
254
		// we either don't know it, or we know it exists but need more details
255
		if ($cachedResponse === null || $cachedResponse === true) {
256
			$this->init();
257
			try {
258
				$response = $this->client->propfind(
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Sabre\HTTP\Client as the method propfind() does only exist in the following sub-classes of Sabre\HTTP\Client: Sabre\DAV\Client, Sabre\DAV\ClientMock. 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...
259
					$this->encodePath($path),
260
					[
261
						'{DAV:}getlastmodified',
262
						'{DAV:}getcontentlength',
263
						'{DAV:}getcontenttype',
264
						'{http://owncloud.org/ns}permissions',
265
						'{http://open-collaboration-services.org/ns}share-permissions',
266
						'{DAV:}resourcetype',
267
						'{DAV:}getetag',
268
					]
269
				);
270
				$this->statCache->set($path, $response);
271
			} catch (ClientHttpException $e) {
272 View Code Duplication
				if ($e->getHttpStatus() === Http::STATUS_NOT_FOUND) {
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...
273
					$this->statCache->clear($path . '/');
274
					$this->statCache->set($path, false);
275
					return false;
276
				}
277
				$this->convertException($e, $path);
278
			} catch (\Exception $e) {
279
				$this->convertException($e, $path);
280
			}
281
		} else {
282
			$response = $cachedResponse;
283
		}
284
		return $response;
285
	}
286
287
	/** {@inheritdoc} */
288
	public function filetype($path) {
289
		try {
290
			$response = $this->propfind($path);
291
			if ($response === false) {
292
				return false;
293
			}
294
			$responseType = [];
295
			if (isset($response["{DAV:}resourcetype"])) {
296
				/** @var ResourceType[] $response */
297
				$responseType = $response["{DAV:}resourcetype"]->getValue();
298
			}
299
			return (\count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
300
		} catch (\Exception $e) {
301
			$this->convertException($e, $path);
302
		}
303
		return false;
304
	}
305
306
	/** {@inheritdoc} */
307
	public function file_exists($path) {
308
		try {
309
			$path = $this->cleanPath($path);
310
			$cachedState = $this->statCache->get($path);
311
			if ($cachedState === false) {
312
				// we know the file doesn't exist
313
				return false;
314
			} elseif ($cachedState !== null) {
315
				return true;
316
			}
317
			// need to get from server
318
			return ($this->propfind($path) !== false);
319
		} catch (\Exception $e) {
320
			$this->convertException($e, $path);
321
		}
322
		return false;
323
	}
324
325
	/** {@inheritdoc} */
326 View Code Duplication
	public function unlink($path) {
327
		$this->init();
328
		$path = $this->cleanPath($path);
329
		$result = $this->simpleResponse('DELETE', $path, null, 204);
330
		$this->statCache->clear($path . '/');
331
		$this->statCache->remove($path);
332
		return $result;
333
	}
334
335
	/** {@inheritdoc} */
336
	public function fopen($path, $mode) {
337
		$this->init();
338
		$path = $this->cleanPath($path);
339
		switch ($mode) {
340
			case 'r':
341
			case 'rb':
342
				try {
343
					$response = $this->httpClientService
344
							->newClient()
345
							->get($this->createBaseUri() . $this->encodePath($path), [
346
									'auth' => [$this->user, $this->password],
347
									'stream' => true
348
							]);
349
				} catch (RequestException $e) {
350
					if ($e->getResponse() instanceof ResponseInterface
351
						&& $e->getResponse()->getStatusCode() === Http::STATUS_NOT_FOUND) {
352
						return false;
353
					} else {
354
						$this->convertException($e);
355
					}
356
				}
357
358
				if ($response->getStatusCode() !== Http::STATUS_OK) {
359
					if ($response->getStatusCode() === Http::STATUS_LOCKED) {
360
						throw new \OCP\Lock\LockedException($path);
361
					} else {
362
						Util::writeLog("webdav client", 'Guzzle get returned status code ' . $response->getStatusCode(), Util::ERROR);
363
						// FIXME: why not returning false here ?!
364
					}
365
				}
366
367
				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 367 which is incompatible with the return type declared by the interface OCP\Files\Storage\IStorage::fopen of type resource|false.
Loading history...
368
			case 'w':
369
			case 'wb':
370
			case 'a':
371
			case 'ab':
372
			case 'r+':
373
			case 'w+':
374
			case 'wb+':
375
			case 'a+':
376
			case 'x':
377
			case 'x+':
378
			case 'c':
379
			case 'c+':
380
				//emulate these
381
				$tempManager = \OC::$server->getTempManager();
382
				if (\strrpos($path, '.') !== false) {
383
					$ext = \substr($path, \strrpos($path, '.'));
384
				} else {
385
					$ext = '';
386
				}
387
				if ($this->file_exists($path)) {
388
					if (!$this->isUpdatable($path)) {
389
						return false;
390
					}
391
					if ($mode === 'w' or $mode === 'w+') {
392
						$tmpFile = $tempManager->getTemporaryFile($ext);
393
					} else {
394
						$tmpFile = $this->getCachedFile($path);
395
					}
396
				} else {
397
					if (!$this->isCreatable(\dirname($path))) {
398
						return false;
399
					}
400
					$tmpFile = $tempManager->getTemporaryFile($ext);
401
				}
402
				Close::registerCallback($tmpFile, [$this, 'writeBack']);
403
				self::$tempFiles[$tmpFile] = $path;
404
				return \fopen('close://' . $tmpFile, $mode);
405
		}
406
	}
407
408
	/**
409
	 * @param string $tmpFile
410
	 */
411
	public function writeBack($tmpFile) {
412 View Code Duplication
		if (isset(self::$tempFiles[$tmpFile])) {
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...
413
			$this->uploadFile($tmpFile, self::$tempFiles[$tmpFile]);
414
			\unlink($tmpFile);
415
		}
416
	}
417
418
	/** {@inheritdoc} */
419
	public function free_space($path) {
420
		$this->init();
421
		$path = $this->cleanPath($path);
422
		try {
423
			// TODO: cacheable ?
424
			$response = $this->client->propfind($this->encodePath($path), ['{DAV:}quota-available-bytes']);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Sabre\HTTP\Client as the method propfind() does only exist in the following sub-classes of Sabre\HTTP\Client: Sabre\DAV\Client, Sabre\DAV\ClientMock. 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...
425
			if ($response === false) {
426
				return FileInfo::SPACE_UNKNOWN;
427
			}
428
			if (isset($response['{DAV:}quota-available-bytes'])) {
429
				return (int)$response['{DAV:}quota-available-bytes'];
430
			} else {
431
				return FileInfo::SPACE_UNKNOWN;
432
			}
433
		} catch (\Exception $e) {
434
			return FileInfo::SPACE_UNKNOWN;
435
		}
436
	}
437
438
	/** {@inheritdoc} */
439
	public function touch($path, $mtime = null) {
440
		$this->init();
441
		if ($mtime === null) {
442
			$mtime = \OC::$server->getTimeFactory()->getTime();
443
		}
444
		$path = $this->cleanPath($path);
445
446
		// if file exists, update the mtime, else create a new empty file
447
		if ($this->file_exists($path)) {
448
			try {
449
				$this->statCache->remove($path);
450
				$this->client->proppatch($this->encodePath($path), ['{DAV:}lastmodified' => $mtime]);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Sabre\HTTP\Client as the method proppatch() does only exist in the following sub-classes of Sabre\HTTP\Client: Sabre\DAV\Client, Sabre\DAV\ClientMock. 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...
451
				// non-owncloud clients might not have accepted the property, need to recheck it
452
				$response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Sabre\HTTP\Client as the method propfind() does only exist in the following sub-classes of Sabre\HTTP\Client: Sabre\DAV\Client, Sabre\DAV\ClientMock. 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...
453
				if ($response === false) {
454
					// file disappeared since ?
455
					return false;
456
				}
457
				if (isset($response['{DAV:}getlastmodified'])) {
458
					$remoteMtime = \strtotime($response['{DAV:}getlastmodified']);
459
					if ($remoteMtime !== $mtime) {
460
						// server has not accepted the mtime
461
						return false;
462
					}
463
				}
464
			} catch (ClientHttpException $e) {
465
				if ($e->getHttpStatus() === 501) {
466
					return false;
467
				}
468
				$this->convertException($e, $path);
469
				return false;
470
			} catch (\Exception $e) {
471
				$this->convertException($e, $path);
472
				return false;
473
			}
474
		} else {
475
			$this->file_put_contents($path, '');
476
		}
477
		return true;
478
	}
479
480
	/**
481
	 * @param string $path
482
	 * @param string $data
483
	 * @return int
484
	 */
485
	public function file_put_contents($path, $data) {
486
		$path = $this->cleanPath($path);
487
		$result = parent::file_put_contents($path, $data);
488
		$this->statCache->remove($path);
489
		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\IStorage::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...
490
	}
491
492
	/**
493
	 * @param string $path
494
	 * @param string $target
495
	 */
496
	protected function uploadFile($path, $target) {
497
		$this->init();
498
499
		// invalidate
500
		$target = $this->cleanPath($target);
501
		$this->statCache->remove($target);
502
		$source = \fopen($path, 'r');
503
504
		$this->removeCachedFile($target);
505
		try {
506
			$this->httpClientService
507
				->newClient()
508
				->put($this->createBaseUri() . $this->encodePath($target), [
509
					'body' => $source,
510
					'auth' => [$this->user, $this->password]
511
				]);
512
		} catch (\Exception $e) {
513
			$this->convertException($e);
514
		}
515
	}
516
517
	/** {@inheritdoc} */
518
	public function rename($path1, $path2) {
519
		$this->init();
520
		$path1 = $this->cleanPath($path1);
521
		$path2 = $this->cleanPath($path2);
522
		try {
523
			// overwrite directory ?
524
			if ($this->is_dir($path2)) {
525
				// needs trailing slash in destination
526
				$path2 = \rtrim($path2, '/') . '/';
527
			}
528
			$this->client->request(
0 ignored issues
show
Bug introduced by
The method request() does not exist on Sabre\HTTP\Client. Did you maybe mean doRequest()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
529
				'MOVE',
530
				$this->encodePath($path1),
531
				null,
532
				[
533
					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
534
				]
535
			);
536
			$this->statCache->clear($path1 . '/');
537
			$this->statCache->clear($path2 . '/');
538
			$this->statCache->set($path1, false);
539
			$this->statCache->set($path2, true);
540
			$this->removeCachedFile($path1);
541
			$this->removeCachedFile($path2);
542
			return true;
543
		} catch (\Exception $e) {
544
			$this->convertException($e);
545
		}
546
		return false;
547
	}
548
549
	/** {@inheritdoc} */
550
	public function copy($path1, $path2) {
551
		$this->init();
552
		$path1 = $this->cleanPath($path1);
553
		$path2 = $this->cleanPath($path2);
554
		try {
555
			// overwrite directory ?
556
			if ($this->is_dir($path2)) {
557
				// needs trailing slash in destination
558
				$path2 = \rtrim($path2, '/') . '/';
559
			}
560
			$this->client->request(
0 ignored issues
show
Bug introduced by
The method request() does not exist on Sabre\HTTP\Client. Did you maybe mean doRequest()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
561
				'COPY',
562
				$this->encodePath($path1),
563
				null,
564
				[
565
					'Destination' => $this->createBaseUri() . $this->encodePath($path2),
566
				]
567
			);
568
			$this->statCache->clear($path2 . '/');
569
			$this->statCache->set($path2, true);
570
			$this->removeCachedFile($path2);
571
			return true;
572
		} catch (\Exception $e) {
573
			$this->convertException($e);
574
		}
575
		return false;
576
	}
577
578
	/** {@inheritdoc} */
579
	public function stat($path) {
580
		try {
581
			$response = $this->propfind($path);
582
			if ($response === false) {
583
				return [];
584
			}
585
			return [
586
				'mtime' => \strtotime($response['{DAV:}getlastmodified']),
587
				'size' => (int)isset($response['{DAV:}getcontentlength']) ? $response['{DAV:}getcontentlength'] : 0,
588
			];
589
		} catch (\Exception $e) {
590
			$this->convertException($e, $path);
591
		}
592
		return [];
593
	}
594
595
	/** {@inheritdoc} */
596
	public function getMimeType($path) {
597
		try {
598
			$response = $this->propfind($path);
599
			if ($response === false) {
600
				return false;
601
			}
602
			$responseType = [];
603
			if (isset($response["{DAV:}resourcetype"])) {
604
				/** @var ResourceType[] $response */
605
				$responseType = $response["{DAV:}resourcetype"]->getValue();
606
			}
607
			$type = (\count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
608
			if ($type == 'dir') {
609
				return 'httpd/unix-directory';
610
			} elseif (isset($response['{DAV:}getcontenttype'])) {
611
				return $response['{DAV:}getcontenttype'];
612
			} else {
613
				return false;
614
			}
615
		} catch (\Exception $e) {
616
			$this->convertException($e, $path);
617
		}
618
		return false;
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
	private 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
	private function simpleResponse($method, $path, $body, $expected) {
655
		$path = $this->cleanPath($path);
656
		try {
657
			$response = $this->client->request($method, $this->encodePath($path), $body);
0 ignored issues
show
Bug introduced by
The method request() does not exist on Sabre\HTTP\Client. Did you maybe mean doRequest()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
658
			return $response['statusCode'] == $expected;
659
		} catch (ClientHttpException $e) {
660 View Code Duplication
			if ($e->getHttpStatus() === Http::STATUS_NOT_FOUND && $method === 'DELETE') {
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...
661
				$this->statCache->clear($path . '/');
662
				$this->statCache->set($path, false);
663
				return false;
664
			}
665
			if ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED && $method === 'MKCOL') {
666
				return false;
667
			}
668
669
			$this->convertException($e, $path);
670
		} catch (\Exception $e) {
671
			$this->convertException($e, $path);
672
		}
673
		return false;
674
	}
675
676
	/**
677
	 * check if curl is installed
678
	 */
679
	public static function checkDependencies() {
680
		return true;
681
	}
682
683
	/** {@inheritdoc} */
684
	public function isUpdatable($path) {
685
		return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
686
	}
687
688
	/** {@inheritdoc} */
689
	public function isCreatable($path) {
690
		return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
691
	}
692
693
	/** {@inheritdoc} */
694
	public function isSharable($path) {
695
		return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
696
	}
697
698
	/** {@inheritdoc} */
699
	public function isDeletable($path) {
700
		return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
701
	}
702
703
	/** {@inheritdoc} */
704
	public function getPermissions($path) {
705
		$this->init();
706
		$path = $this->cleanPath($path);
707
		$response = $this->propfind($path);
708
		if ($response === false) {
709
			return 0;
710
		}
711
		if (isset($response['{http://owncloud.org/ns}permissions'])) {
712
			return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
713
		} elseif ($this->is_dir($path)) {
714
			return Constants::PERMISSION_ALL;
715
		} elseif ($this->file_exists($path)) {
716
			return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
717
		} else {
718
			return 0;
719
		}
720
	}
721
722
	/** {@inheritdoc} */
723
	public function getETag($path) {
724
		$this->init();
725
		$path = $this->cleanPath($path);
726
		$response = $this->propfind($path);
727
		if ($response === false) {
728
			return null;
729
		}
730
		if (isset($response['{DAV:}getetag'])) {
731
			return \trim($response['{DAV:}getetag'], '"');
732
		}
733
		return parent::getEtag($path);
734
	}
735
736
	/**
737
	 * @param string $permissionsString
738
	 * @return int
739
	 */
740
	protected function parsePermissions($permissionsString) {
741
		$permissions = Constants::PERMISSION_READ;
742
		if (\strpos($permissionsString, 'R') !== false) {
743
			$permissions |= Constants::PERMISSION_SHARE;
744
		}
745
		if (\strpos($permissionsString, 'D') !== false) {
746
			$permissions |= Constants::PERMISSION_DELETE;
747
		}
748
		if (\strpos($permissionsString, 'W') !== false) {
749
			$permissions |= Constants::PERMISSION_UPDATE;
750
		}
751
		if (\strpos($permissionsString, 'CK') !== false) {
752
			$permissions |= Constants::PERMISSION_CREATE;
753
			$permissions |= Constants::PERMISSION_UPDATE;
754
		}
755
		return $permissions;
756
	}
757
758
	/**
759
	 * check if a file or folder has been updated since $time
760
	 *
761
	 * @param string $path
762
	 * @param int $time
763
	 * @throws \OCP\Files\StorageNotAvailableException
764
	 * @return bool
765
	 */
766
	public function hasUpdated($path, $time) {
767
		$this->init();
768
		$path = $this->cleanPath($path);
769
		try {
770
			// force refresh for $path
771
			$this->statCache->remove($path);
772
			$response = $this->propfind($path);
773
			if ($response === false) {
774
				if ($path === '') {
775
					// if root is gone it means the storage is not available
776
					throw new StorageNotAvailableException('remote root vanished');
777
				}
778
				return false;
779
			}
780
			if (isset($response['{DAV:}getetag'])) {
781
				$cachedData = $this->getCache()->get($path);
782
				$etag = \trim($response['{DAV:}getetag'], '"');
783
				if (!empty($etag) && $cachedData['etag'] !== $etag) {
784
					return true;
785
				} elseif (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
				} elseif (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) {
799
			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
	private function convertException(Exception $e, $path = '') {
829
		\OC::$server->getLogger()->logException($e);
830
		Util::writeLog('files_external', $e->getMessage(), Util::ERROR);
831
		if ($e instanceof ClientHttpException) {
832
			$this->throwByStatusCode($e->getHttpStatus(), $e, $path);
833
		} elseif ($e instanceof \GuzzleHttp\Exception\RequestException) {
834
			if ($e->getResponse() instanceof ResponseInterface) {
835
				$this->throwByStatusCode($e->getResponse()->getStatusCode(), $e);
836
			}
837
			// connection timeout or refused, server could be temporarily down
838
			throw new StorageNotAvailableException(\get_class($e) . ': ' . $e->getMessage());
839
		} elseif ($e instanceof \InvalidArgumentException) {
840
			// parse error because the server returned HTML instead of XML,
841
			// possibly temporarily down
842
			throw new StorageNotAvailableException(\get_class($e) . ': ' . $e->getMessage());
843
		} elseif (($e instanceof StorageNotAvailableException)
844
			|| ($e instanceof StorageInvalidException)
845
			|| ($e instanceof \Sabre\DAV\Exception
846
		)) {
847
			// rethrow
848
			throw $e;
849
		}
850
851
		// TODO: only log for now, but in the future need to wrap/rethrow exception
852
	}
853
854
	/**
855
	 * Throw exception by status code
856
	 *
857
	 * @param int $statusCode status code
858
	 * @param string $path optional path for some exceptions
859
	 * @throws \Exception Sabre or ownCloud exceptions
860
	 */
861
	private function throwByStatusCode($statusCode, $e, $path = '') {
862
		switch ($statusCode) {
863
			case Http::STATUS_LOCKED:
864
				throw new \OCP\Lock\LockedException($path);
865
			case Http::STATUS_UNAUTHORIZED:
866
				// either password was changed or was invalid all along
867
				throw new StorageInvalidException(\get_class($e) . ': ' . $e->getMessage());
868
			case Http::STATUS_INSUFFICIENT_STORAGE:
869
				throw new InsufficientStorage();
870
			case Http::STATUS_FORBIDDEN:
871
				throw new Forbidden('Forbidden');
872
		}
873
		throw new StorageNotAvailableException(\get_class($e) . ': ' . $e->getMessage());
874
	}
875
876
	/**
877
	 * @param string $path
878
	 * @param array $options
879
	 * @return StreamInterface
880
	 * @since 11.0.0
881
	 */
882
	public function readFile(string $path, array $options = []): StreamInterface {
883
		$this->init();
884
		$path = $this->cleanPath($path);
885
		$response = $this->httpClientService
886
			->newClient()
887
			->get($this->createBaseUri() . $this->encodePath($path), [
888
				'auth' => [$this->user, $this->password],
889
				'stream' => true
890
			]);
891
		return stream_for($response->getBody());
892
	}
893
894
	/**
895
	 * @param string $path
896
	 * @param StreamInterface $stream
897
	 * @return int
898
	 * @since 11.0.0
899
	 */
900
	public function writeFile(string $path, StreamInterface $stream): int {
901
		$this->init();
902
		$path = $this->cleanPath($path);
903
904
		if ($this->file_exists($path)) {
905
			if (!$this->isUpdatable($path)) {
906
				throw new \OCP\Files\ForbiddenException();
0 ignored issues
show
Bug introduced by
The call to ForbiddenException::__construct() misses some required arguments starting with $message.
Loading history...
907
			}
908
		} else {
909
			if (!$this->isCreatable(\dirname($path))) {
910
				throw new \OCP\Files\ForbiddenException();
0 ignored issues
show
Bug introduced by
The call to ForbiddenException::__construct() misses some required arguments starting with $message.
Loading history...
911
			}
912
		}
913
914
		try {
915
			$size = $stream->getSize();
916
			$this->httpClientService
917
				->newClient()
918
				->put($this->createBaseUri() . $this->encodePath($path), [
919
					'body' => $stream->detach(),
920
					'auth' => [$this->user, $this->password]
921
				]);
922
			return $size;
923
		} catch (\Exception $e) {
924
			$this->convertException($e);
925
		}
926
	}
927
}
928