Manager::getAccessList()   F
last analyzed

Complexity

Conditions 18
Paths 257

Size

Total Lines 83
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 18
eloc 52
nc 257
nop 3
dl 0
loc 83
rs 3.3208
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
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Bjoern Schiessle <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Christoph Wurst <[email protected]>
9
 * @author Daniel Calviño Sánchez <[email protected]>
10
 * @author Daniel Kesselberg <[email protected]>
11
 * @author Jan-Christoph Borchardt <[email protected]>
12
 * @author Joas Schilling <[email protected]>
13
 * @author John Molakvoæ <[email protected]>
14
 * @author Julius Härtl <[email protected]>
15
 * @author Lukas Reschke <[email protected]>
16
 * @author Maxence Lange <[email protected]>
17
 * @author Maxence Lange <[email protected]>
18
 * @author Morris Jobke <[email protected]>
19
 * @author Pauli Järvinen <[email protected]>
20
 * @author Robin Appelman <[email protected]>
21
 * @author Roeland Jago Douma <[email protected]>
22
 * @author Samuel <[email protected]>
23
 * @author szaimen <[email protected]>
24
 * @author Valdnet <[email protected]>
25
 * @author Vincent Petry <[email protected]>
26
 *
27
 * @license AGPL-3.0
28
 *
29
 * This code is free software: you can redistribute it and/or modify
30
 * it under the terms of the GNU Affero General Public License, version 3,
31
 * as published by the Free Software Foundation.
32
 *
33
 * This program is distributed in the hope that it will be useful,
34
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
35
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
36
 * GNU Affero General Public License for more details.
37
 *
38
 * You should have received a copy of the GNU Affero General Public License, version 3,
39
 * along with this program. If not, see <http://www.gnu.org/licenses/>
40
 *
41
 */
42
namespace OC\Share20;
43
44
use OCP\Cache\CappedMemoryCache;
45
use OC\Files\Mount\MoveableMount;
46
use OC\KnownUser\KnownUserService;
47
use OC\Share20\Exception\ProviderException;
48
use OCA\Files_Sharing\AppInfo\Application;
49
use OCA\Files_Sharing\ISharedStorage;
50
use OCP\EventDispatcher\IEventDispatcher;
51
use OCP\Files\File;
52
use OCP\Files\Folder;
53
use OCP\Files\IRootFolder;
54
use OCP\Files\Mount\IMountManager;
55
use OCP\Files\Node;
56
use OCP\HintException;
57
use OCP\IConfig;
58
use OCP\IGroupManager;
59
use OCP\IL10N;
60
use OCP\IURLGenerator;
61
use OCP\IUser;
62
use OCP\IUserManager;
63
use OCP\IUserSession;
64
use OCP\L10N\IFactory;
65
use OCP\Mail\IMailer;
66
use OCP\Security\Events\ValidatePasswordPolicyEvent;
67
use OCP\Security\IHasher;
68
use OCP\Security\ISecureRandom;
69
use OCP\Share;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, OC\Share20\Share. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
70
use OCP\Share\Exceptions\AlreadySharedException;
71
use OCP\Share\Exceptions\GenericShareException;
72
use OCP\Share\Exceptions\ShareNotFound;
73
use OCP\Share\IManager;
74
use OCP\Share\IProviderFactory;
75
use OCP\Share\IShare;
76
use OCP\Share\IShareProvider;
77
use Psr\Log\LoggerInterface;
78
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
79
use Symfony\Component\EventDispatcher\GenericEvent;
80
81
/**
82
 * This class is the communication hub for all sharing related operations.
83
 */
84
class Manager implements IManager {
85
	/** @var IProviderFactory */
86
	private $factory;
87
	private LoggerInterface $logger;
88
	/** @var IConfig */
89
	private $config;
90
	/** @var ISecureRandom */
91
	private $secureRandom;
92
	/** @var IHasher */
93
	private $hasher;
94
	/** @var IMountManager */
95
	private $mountManager;
96
	/** @var IGroupManager */
97
	private $groupManager;
98
	/** @var IL10N */
99
	private $l;
100
	/** @var IFactory */
101
	private $l10nFactory;
102
	/** @var IUserManager */
103
	private $userManager;
104
	/** @var IRootFolder */
105
	private $rootFolder;
106
	/** @var CappedMemoryCache */
107
	private $sharingDisabledForUsersCache;
108
	/** @var EventDispatcherInterface */
109
	private $legacyDispatcher;
110
	/** @var LegacyHooks */
111
	private $legacyHooks;
112
	/** @var IMailer */
113
	private $mailer;
114
	/** @var IURLGenerator */
115
	private $urlGenerator;
116
	/** @var \OC_Defaults */
117
	private $defaults;
118
	/** @var IEventDispatcher */
119
	private $dispatcher;
120
	/** @var IUserSession */
121
	private $userSession;
122
	/** @var KnownUserService */
123
	private $knownUserService;
124
125
	public function __construct(
126
		LoggerInterface $logger,
127
		IConfig $config,
128
		ISecureRandom $secureRandom,
129
		IHasher $hasher,
130
		IMountManager $mountManager,
131
		IGroupManager $groupManager,
132
		IL10N $l,
133
		IFactory $l10nFactory,
134
		IProviderFactory $factory,
135
		IUserManager $userManager,
136
		IRootFolder $rootFolder,
137
		EventDispatcherInterface $legacyDispatcher,
138
		IMailer $mailer,
139
		IURLGenerator $urlGenerator,
140
		\OC_Defaults $defaults,
141
		IEventDispatcher $dispatcher,
142
		IUserSession $userSession,
143
		KnownUserService $knownUserService
144
	) {
145
		$this->logger = $logger;
146
		$this->config = $config;
147
		$this->secureRandom = $secureRandom;
148
		$this->hasher = $hasher;
149
		$this->mountManager = $mountManager;
150
		$this->groupManager = $groupManager;
151
		$this->l = $l;
152
		$this->l10nFactory = $l10nFactory;
153
		$this->factory = $factory;
154
		$this->userManager = $userManager;
155
		$this->rootFolder = $rootFolder;
156
		$this->legacyDispatcher = $legacyDispatcher;
157
		$this->sharingDisabledForUsersCache = new CappedMemoryCache();
158
		// The constructor of LegacyHooks registers the listeners of share events
159
		// do not remove if those are not properly migrated
160
		$this->legacyHooks = new LegacyHooks($this->legacyDispatcher);
161
		$this->mailer = $mailer;
162
		$this->urlGenerator = $urlGenerator;
163
		$this->defaults = $defaults;
164
		$this->dispatcher = $dispatcher;
165
		$this->userSession = $userSession;
166
		$this->knownUserService = $knownUserService;
167
	}
168
169
	/**
170
	 * Convert from a full share id to a tuple (providerId, shareId)
171
	 *
172
	 * @param string $id
173
	 * @return string[]
174
	 */
175
	private function splitFullId($id) {
176
		return explode(':', $id, 2);
177
	}
178
179
	/**
180
	 * Verify if a password meets all requirements
181
	 *
182
	 * @param string $password
183
	 * @throws \Exception
184
	 */
185
	protected function verifyPassword($password) {
186
		if ($password === null) {
0 ignored issues
show
introduced by
The condition $password === null is always false.
Loading history...
187
			// No password is set, check if this is allowed.
188
			if ($this->shareApiLinkEnforcePassword()) {
189
				throw new \InvalidArgumentException('Passwords are enforced for link and mail shares');
190
			}
191
192
			return;
193
		}
194
195
		// Let others verify the password
196
		try {
197
			$this->legacyDispatcher->dispatch(new ValidatePasswordPolicyEvent($password));
198
		} catch (HintException $e) {
199
			throw new \Exception($e->getHint());
200
		}
201
	}
202
203
	/**
204
	 * Check for generic requirements before creating a share
205
	 *
206
	 * @param IShare $share
207
	 * @throws \InvalidArgumentException
208
	 * @throws GenericShareException
209
	 *
210
	 * @suppress PhanUndeclaredClassMethod
211
	 */
212
	protected function generalCreateChecks(IShare $share) {
213
		if ($share->getShareType() === IShare::TYPE_USER) {
214
			// We expect a valid user as sharedWith for user shares
215
			if (!$this->userManager->userExists($share->getSharedWith())) {
216
				throw new \InvalidArgumentException('SharedWith is not a valid user');
217
			}
218
		} elseif ($share->getShareType() === IShare::TYPE_GROUP) {
219
			// We expect a valid group as sharedWith for group shares
220
			if (!$this->groupManager->groupExists($share->getSharedWith())) {
221
				throw new \InvalidArgumentException('SharedWith is not a valid group');
222
			}
223
		} elseif ($share->getShareType() === IShare::TYPE_LINK) {
224
			// No check for TYPE_EMAIL here as we have a recipient for them
225
			if ($share->getSharedWith() !== null) {
0 ignored issues
show
introduced by
The condition $share->getSharedWith() !== null is always true.
Loading history...
226
				throw new \InvalidArgumentException('SharedWith should be empty');
227
			}
228
		} elseif ($share->getShareType() === IShare::TYPE_EMAIL) {
229
			if ($share->getSharedWith() === null) {
0 ignored issues
show
introduced by
The condition $share->getSharedWith() === null is always false.
Loading history...
230
				throw new \InvalidArgumentException('SharedWith should not be empty');
231
			}
232
		} elseif ($share->getShareType() === IShare::TYPE_REMOTE) {
233
			if ($share->getSharedWith() === null) {
0 ignored issues
show
introduced by
The condition $share->getSharedWith() === null is always false.
Loading history...
234
				throw new \InvalidArgumentException('SharedWith should not be empty');
235
			}
236
		} elseif ($share->getShareType() === IShare::TYPE_REMOTE_GROUP) {
237
			if ($share->getSharedWith() === null) {
0 ignored issues
show
introduced by
The condition $share->getSharedWith() === null is always false.
Loading history...
238
				throw new \InvalidArgumentException('SharedWith should not be empty');
239
			}
240
		} elseif ($share->getShareType() === IShare::TYPE_CIRCLE) {
241
			$circle = \OCA\Circles\Api\v1\Circles::detailsCircle($share->getSharedWith());
0 ignored issues
show
Bug introduced by
The type OCA\Circles\Api\v1\Circles was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
242
			if ($circle === null) {
243
				throw new \InvalidArgumentException('SharedWith is not a valid circle');
244
			}
245
		} elseif ($share->getShareType() === IShare::TYPE_ROOM) {
246
		} elseif ($share->getShareType() === IShare::TYPE_DECK) {
247
		} elseif ($share->getShareType() === IShare::TYPE_SCIENCEMESH) {
248
		} else {
249
			// We cannot handle other types yet
250
			throw new \InvalidArgumentException('unknown share type');
251
		}
252
253
		// Verify the initiator of the share is set
254
		if ($share->getSharedBy() === null) {
0 ignored issues
show
introduced by
The condition $share->getSharedBy() === null is always false.
Loading history...
255
			throw new \InvalidArgumentException('SharedBy should be set');
256
		}
257
258
		// Cannot share with yourself
259
		if ($share->getShareType() === IShare::TYPE_USER &&
260
			$share->getSharedWith() === $share->getSharedBy()) {
261
			throw new \InvalidArgumentException('Cannot share with yourself');
262
		}
263
264
		// The path should be set
265
		if ($share->getNode() === null) {
266
			throw new \InvalidArgumentException('Path should be set');
267
		}
268
269
		// And it should be a file or a folder
270
		if (!($share->getNode() instanceof \OCP\Files\File) &&
271
			!($share->getNode() instanceof \OCP\Files\Folder)) {
0 ignored issues
show
introduced by
$share->getNode() is always a sub-type of OCP\Files\Folder.
Loading history...
272
			throw new \InvalidArgumentException('Path should be either a file or a folder');
273
		}
274
275
		// And you cannot share your rootfolder
276
		if ($this->userManager->userExists($share->getSharedBy())) {
277
			$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
278
		} else {
279
			$userFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
280
		}
281
		if ($userFolder->getId() === $share->getNode()->getId()) {
282
			throw new \InvalidArgumentException('You cannot share your root folder');
283
		}
284
285
		// Check if we actually have share permissions
286
		if (!$share->getNode()->isShareable()) {
287
			$message_t = $this->l->t('You are not allowed to share %s', [$share->getNode()->getName()]);
288
			throw new GenericShareException($message_t, $message_t, 404);
289
		}
290
291
		// Permissions should be set
292
		if ($share->getPermissions() === null) {
0 ignored issues
show
introduced by
The condition $share->getPermissions() === null is always false.
Loading history...
293
			throw new \InvalidArgumentException('A share requires permissions');
294
		}
295
296
		$isFederatedShare = $share->getNode()->getStorage()->instanceOfStorage('\OCA\Files_Sharing\External\Storage');
297
		$permissions = 0;
298
299
		if (!$isFederatedShare && $share->getNode()->getOwner() && $share->getNode()->getOwner()->getUID() !== $share->getSharedBy()) {
300
			$userMounts = array_filter($userFolder->getById($share->getNode()->getId()), function ($mount) {
301
				// We need to filter since there might be other mountpoints that contain the file
302
				// e.g. if the user has access to the same external storage that the file is originating from
303
				return $mount->getStorage()->instanceOfStorage(ISharedStorage::class);
304
			});
305
			$userMount = array_shift($userMounts);
306
			if ($userMount === null) {
307
				throw new GenericShareException('Could not get proper share mount for ' . $share->getNode()->getId() . '. Failing since else the next calls are called with null');
308
			}
309
			$mount = $userMount->getMountPoint();
310
			// When it's a reshare use the parent share permissions as maximum
311
			$userMountPointId = $mount->getStorageRootId();
312
			$userMountPoints = $userFolder->getById($userMountPointId);
313
			$userMountPoint = array_shift($userMountPoints);
314
315
			if ($userMountPoint === null) {
316
				throw new GenericShareException('Could not get proper user mount for ' . $userMountPointId . '. Failing since else the next calls are called with null');
317
			}
318
319
			/* Check if this is an incoming share */
320
			$incomingShares = $this->getSharedWith($share->getSharedBy(), IShare::TYPE_USER, $userMountPoint, -1, 0);
321
			$incomingShares = array_merge($incomingShares, $this->getSharedWith($share->getSharedBy(), IShare::TYPE_GROUP, $userMountPoint, -1, 0));
322
			$incomingShares = array_merge($incomingShares, $this->getSharedWith($share->getSharedBy(), IShare::TYPE_CIRCLE, $userMountPoint, -1, 0));
323
			$incomingShares = array_merge($incomingShares, $this->getSharedWith($share->getSharedBy(), IShare::TYPE_ROOM, $userMountPoint, -1, 0));
324
325
			/** @var IShare[] $incomingShares */
326
			if (!empty($incomingShares)) {
327
				foreach ($incomingShares as $incomingShare) {
328
					$permissions |= $incomingShare->getPermissions();
329
				}
330
			}
331
		} else {
332
			/*
333
			 * Quick fix for #23536
334
			 * Non moveable mount points do not have update and delete permissions
335
			 * while we 'most likely' do have that on the storage.
336
			 */
337
			$permissions = $share->getNode()->getPermissions();
338
			if (!($share->getNode()->getMountPoint() instanceof MoveableMount)) {
339
				$permissions |= \OCP\Constants::PERMISSION_DELETE | \OCP\Constants::PERMISSION_UPDATE;
340
			}
341
		}
342
343
		// Check that we do not share with more permissions than we have
344
		if ($share->getPermissions() & ~$permissions) {
345
			$path = $userFolder->getRelativePath($share->getNode()->getPath());
346
			$message_t = $this->l->t('Cannot increase permissions of %s', [$path]);
347
			throw new GenericShareException($message_t, $message_t, 404);
348
		}
349
350
351
		// Check that read permissions are always set
352
		// Link shares are allowed to have no read permissions to allow upload to hidden folders
353
		$noReadPermissionRequired = $share->getShareType() === IShare::TYPE_LINK
354
			|| $share->getShareType() === IShare::TYPE_EMAIL;
355
		if (!$noReadPermissionRequired &&
356
			($share->getPermissions() & \OCP\Constants::PERMISSION_READ) === 0) {
357
			throw new \InvalidArgumentException('Shares need at least read permissions');
358
		}
359
360
		if ($share->getNode() instanceof \OCP\Files\File) {
361
			if ($share->getPermissions() & \OCP\Constants::PERMISSION_DELETE) {
362
				$message_t = $this->l->t('Files cannot be shared with delete permissions');
363
				throw new GenericShareException($message_t);
364
			}
365
			if ($share->getPermissions() & \OCP\Constants::PERMISSION_CREATE) {
366
				$message_t = $this->l->t('Files cannot be shared with create permissions');
367
				throw new GenericShareException($message_t);
368
			}
369
		}
370
	}
371
372
	/**
373
	 * Validate if the expiration date fits the system settings
374
	 *
375
	 * @param IShare $share The share to validate the expiration date of
376
	 * @return IShare The modified share object
377
	 * @throws GenericShareException
378
	 * @throws \InvalidArgumentException
379
	 * @throws \Exception
380
	 */
381
	protected function validateExpirationDateInternal(IShare $share) {
382
		$isRemote = $share->getShareType() === IShare::TYPE_REMOTE || $share->getShareType() === IShare::TYPE_REMOTE_GROUP;
383
384
		$expirationDate = $share->getExpirationDate();
385
386
		if ($expirationDate !== null) {
387
			//Make sure the expiration date is a date
388
			$expirationDate->setTime(0, 0, 0);
389
390
			$date = new \DateTime();
391
			$date->setTime(0, 0, 0);
392
			if ($date >= $expirationDate) {
393
				$message = $this->l->t('Expiration date is in the past');
394
				throw new GenericShareException($message, $message, 404);
395
			}
396
		}
397
398
		// If expiredate is empty set a default one if there is a default
399
		$fullId = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $fullId is dead and can be removed.
Loading history...
400
		try {
401
			$fullId = $share->getFullId();
402
		} catch (\UnexpectedValueException $e) {
403
			// This is a new share
404
		}
405
406
		if ($isRemote) {
407
			$defaultExpireDate = $this->shareApiRemoteDefaultExpireDate();
408
			$defaultExpireDays = $this->shareApiRemoteDefaultExpireDays();
409
			$configProp = 'remote_defaultExpDays';
410
			$isEnforced = $this->shareApiRemoteDefaultExpireDateEnforced();
411
		} else {
412
			$defaultExpireDate = $this->shareApiInternalDefaultExpireDate();
413
			$defaultExpireDays = $this->shareApiInternalDefaultExpireDays();
414
			$configProp = 'internal_defaultExpDays';
415
			$isEnforced = $this->shareApiInternalDefaultExpireDateEnforced();
416
		}
417
		if ($fullId === null && $expirationDate === null && $defaultExpireDate) {
0 ignored issues
show
introduced by
The condition $fullId === null is always false.
Loading history...
418
			$expirationDate = new \DateTime();
419
			$expirationDate->setTime(0, 0, 0);
420
421
			$days = (int)$this->config->getAppValue('core', $configProp, (string)$defaultExpireDays);
422
			if ($days > $defaultExpireDays) {
423
				$days = $defaultExpireDays;
424
			}
425
			$expirationDate->add(new \DateInterval('P' . $days . 'D'));
426
		}
427
428
		// If we enforce the expiration date check that is does not exceed
429
		if ($isEnforced) {
430
			if ($expirationDate === null) {
431
				throw new \InvalidArgumentException('Expiration date is enforced');
432
			}
433
434
			$date = new \DateTime();
435
			$date->setTime(0, 0, 0);
436
			$date->add(new \DateInterval('P' . $defaultExpireDays . 'D'));
437
			if ($date < $expirationDate) {
438
				$message = $this->l->n('Cannot set expiration date more than %n day in the future', 'Cannot set expiration date more than %n days in the future', $defaultExpireDays);
439
				throw new GenericShareException($message, $message, 404);
440
			}
441
		}
442
443
		$accepted = true;
444
		$message = '';
445
		\OCP\Util::emitHook('\OC\Share', 'verifyExpirationDate', [
446
			'expirationDate' => &$expirationDate,
447
			'accepted' => &$accepted,
448
			'message' => &$message,
449
			'passwordSet' => $share->getPassword() !== null,
450
		]);
451
452
		if (!$accepted) {
0 ignored issues
show
introduced by
The condition $accepted is always true.
Loading history...
453
			throw new \Exception($message);
454
		}
455
456
		$share->setExpirationDate($expirationDate);
457
458
		return $share;
459
	}
460
461
	/**
462
	 * Validate if the expiration date fits the system settings
463
	 *
464
	 * @param IShare $share The share to validate the expiration date of
465
	 * @return IShare The modified share object
466
	 * @throws GenericShareException
467
	 * @throws \InvalidArgumentException
468
	 * @throws \Exception
469
	 */
470
	protected function validateExpirationDateLink(IShare $share) {
471
		$expirationDate = $share->getExpirationDate();
472
473
		if ($expirationDate !== null) {
474
			//Make sure the expiration date is a date
475
			$expirationDate->setTime(0, 0, 0);
476
477
			$date = new \DateTime();
478
			$date->setTime(0, 0, 0);
479
			if ($date >= $expirationDate) {
480
				$message = $this->l->t('Expiration date is in the past');
481
				throw new GenericShareException($message, $message, 404);
482
			}
483
		}
484
485
		// If expiredate is empty set a default one if there is a default
486
		$fullId = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $fullId is dead and can be removed.
Loading history...
487
		try {
488
			$fullId = $share->getFullId();
489
		} catch (\UnexpectedValueException $e) {
490
			// This is a new share
491
		}
492
493
		if ($fullId === null && $expirationDate === null && $this->shareApiLinkDefaultExpireDate()) {
0 ignored issues
show
introduced by
The condition $fullId === null is always false.
Loading history...
494
			$expirationDate = new \DateTime();
495
			$expirationDate->setTime(0, 0, 0);
496
497
			$days = (int)$this->config->getAppValue('core', 'link_defaultExpDays', (string)$this->shareApiLinkDefaultExpireDays());
498
			if ($days > $this->shareApiLinkDefaultExpireDays()) {
499
				$days = $this->shareApiLinkDefaultExpireDays();
500
			}
501
			$expirationDate->add(new \DateInterval('P' . $days . 'D'));
502
		}
503
504
		// If we enforce the expiration date check that is does not exceed
505
		if ($this->shareApiLinkDefaultExpireDateEnforced()) {
506
			if ($expirationDate === null) {
507
				throw new \InvalidArgumentException('Expiration date is enforced');
508
			}
509
510
			$date = new \DateTime();
511
			$date->setTime(0, 0, 0);
512
			$date->add(new \DateInterval('P' . $this->shareApiLinkDefaultExpireDays() . 'D'));
513
			if ($date < $expirationDate) {
514
				$message = $this->l->n('Cannot set expiration date more than %n day in the future', 'Cannot set expiration date more than %n days in the future', $this->shareApiLinkDefaultExpireDays());
515
				throw new GenericShareException($message, $message, 404);
516
			}
517
		}
518
519
		$accepted = true;
520
		$message = '';
521
		\OCP\Util::emitHook('\OC\Share', 'verifyExpirationDate', [
522
			'expirationDate' => &$expirationDate,
523
			'accepted' => &$accepted,
524
			'message' => &$message,
525
			'passwordSet' => $share->getPassword() !== null,
526
		]);
527
528
		if (!$accepted) {
0 ignored issues
show
introduced by
The condition $accepted is always true.
Loading history...
529
			throw new \Exception($message);
530
		}
531
532
		$share->setExpirationDate($expirationDate);
533
534
		return $share;
535
	}
536
537
	/**
538
	 * Check for pre share requirements for user shares
539
	 *
540
	 * @param IShare $share
541
	 * @throws \Exception
542
	 */
543
	protected function userCreateChecks(IShare $share) {
544
		// Check if we can share with group members only
545
		if ($this->shareWithGroupMembersOnly()) {
546
			$sharedBy = $this->userManager->get($share->getSharedBy());
547
			$sharedWith = $this->userManager->get($share->getSharedWith());
548
			// Verify we can share with this user
549
			$groups = array_intersect(
550
				$this->groupManager->getUserGroupIds($sharedBy),
551
				$this->groupManager->getUserGroupIds($sharedWith)
552
			);
553
			if (empty($groups)) {
554
				$message_t = $this->l->t('Sharing is only allowed with group members');
555
				throw new \Exception($message_t);
556
			}
557
		}
558
559
		/*
560
		 * TODO: Could be costly, fix
561
		 *
562
		 * Also this is not what we want in the future.. then we want to squash identical shares.
563
		 */
564
		$provider = $this->factory->getProviderForType(IShare::TYPE_USER);
565
		$existingShares = $provider->getSharesByPath($share->getNode());
566
		foreach ($existingShares as $existingShare) {
567
			// Ignore if it is the same share
568
			try {
569
				if ($existingShare->getFullId() === $share->getFullId()) {
570
					continue;
571
				}
572
			} catch (\UnexpectedValueException $e) {
573
				//Shares are not identical
574
			}
575
576
			// Identical share already exists
577
			if ($existingShare->getSharedWith() === $share->getSharedWith() && $existingShare->getShareType() === $share->getShareType()) {
578
				$message = $this->l->t('Sharing %s failed, because this item is already shared with user %s', [$share->getNode()->getName(), $share->getSharedWithDisplayName()]);
579
				throw new AlreadySharedException($message, $existingShare);
580
			}
581
582
			// The share is already shared with this user via a group share
583
			if ($existingShare->getShareType() === IShare::TYPE_GROUP) {
584
				$group = $this->groupManager->get($existingShare->getSharedWith());
585
				if (!is_null($group)) {
586
					$user = $this->userManager->get($share->getSharedWith());
587
588
					if ($group->inGroup($user) && $existingShare->getShareOwner() !== $share->getShareOwner()) {
589
						$message = $this->l->t('Sharing %s failed, because this item is already shared with user %s', [$share->getNode()->getName(), $share->getSharedWithDisplayName()]);
590
						throw new AlreadySharedException($message, $existingShare);
591
					}
592
				}
593
			}
594
		}
595
	}
596
597
	/**
598
	 * Check for pre share requirements for group shares
599
	 *
600
	 * @param IShare $share
601
	 * @throws \Exception
602
	 */
603
	protected function groupCreateChecks(IShare $share) {
604
		// Verify group shares are allowed
605
		if (!$this->allowGroupSharing()) {
606
			throw new \Exception('Group sharing is now allowed');
607
		}
608
609
		// Verify if the user can share with this group
610
		if ($this->shareWithGroupMembersOnly()) {
611
			$sharedBy = $this->userManager->get($share->getSharedBy());
612
			$sharedWith = $this->groupManager->get($share->getSharedWith());
613
			if (is_null($sharedWith) || !$sharedWith->inGroup($sharedBy)) {
614
				throw new \Exception('Sharing is only allowed within your own groups');
615
			}
616
		}
617
618
		/*
619
		 * TODO: Could be costly, fix
620
		 *
621
		 * Also this is not what we want in the future.. then we want to squash identical shares.
622
		 */
623
		$provider = $this->factory->getProviderForType(IShare::TYPE_GROUP);
624
		$existingShares = $provider->getSharesByPath($share->getNode());
625
		foreach ($existingShares as $existingShare) {
626
			try {
627
				if ($existingShare->getFullId() === $share->getFullId()) {
628
					continue;
629
				}
630
			} catch (\UnexpectedValueException $e) {
631
				//It is a new share so just continue
632
			}
633
634
			if ($existingShare->getSharedWith() === $share->getSharedWith() && $existingShare->getShareType() === $share->getShareType()) {
635
				throw new AlreadySharedException('Path is already shared with this group', $existingShare);
636
			}
637
		}
638
	}
639
640
	/**
641
	 * Check for pre share requirements for link shares
642
	 *
643
	 * @param IShare $share
644
	 * @throws \Exception
645
	 */
646
	protected function linkCreateChecks(IShare $share) {
647
		// Are link shares allowed?
648
		if (!$this->shareApiAllowLinks()) {
649
			throw new \Exception('Link sharing is not allowed');
650
		}
651
652
		// Check if public upload is allowed
653
		if ($share->getNodeType() === 'folder' && !$this->shareApiLinkAllowPublicUpload() &&
654
			($share->getPermissions() & (\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE))) {
655
			throw new \InvalidArgumentException('Public upload is not allowed');
656
		}
657
	}
658
659
	/**
660
	 * To make sure we don't get invisible link shares we set the parent
661
	 * of a link if it is a reshare. This is a quick word around
662
	 * until we can properly display multiple link shares in the UI
663
	 *
664
	 * See: https://github.com/owncloud/core/issues/22295
665
	 *
666
	 * FIXME: Remove once multiple link shares can be properly displayed
667
	 *
668
	 * @param IShare $share
669
	 */
670
	protected function setLinkParent(IShare $share) {
671
		// No sense in checking if the method is not there.
672
		if (method_exists($share, 'setParent')) {
673
			$storage = $share->getNode()->getStorage();
674
			if ($storage->instanceOfStorage('\OCA\Files_Sharing\ISharedStorage')) {
675
				/** @var \OCA\Files_Sharing\SharedStorage $storage */
676
				$share->setParent($storage->getShareId());
677
			}
678
		}
679
	}
680
681
	/**
682
	 * @param File|Folder $path
683
	 */
684
	protected function pathCreateChecks($path) {
685
		// Make sure that we do not share a path that contains a shared mountpoint
686
		if ($path instanceof \OCP\Files\Folder) {
687
			$mounts = $this->mountManager->findIn($path->getPath());
688
			foreach ($mounts as $mount) {
689
				if ($mount->getStorage()->instanceOfStorage('\OCA\Files_Sharing\ISharedStorage')) {
690
					throw new \InvalidArgumentException('Path contains files shared with you');
691
				}
692
			}
693
		}
694
	}
695
696
	/**
697
	 * Check if the user that is sharing can actually share
698
	 *
699
	 * @param IShare $share
700
	 * @throws \Exception
701
	 */
702
	protected function canShare(IShare $share) {
703
		if (!$this->shareApiEnabled()) {
704
			throw new \Exception('Sharing is disabled');
705
		}
706
707
		if ($this->sharingDisabledForUser($share->getSharedBy())) {
708
			throw new \Exception('Sharing is disabled for you');
709
		}
710
	}
711
712
	/**
713
	 * Share a path
714
	 *
715
	 * @param IShare $share
716
	 * @return IShare The share object
717
	 * @throws \Exception
718
	 *
719
	 * TODO: handle link share permissions or check them
720
	 */
721
	public function createShare(IShare $share) {
722
		$this->canShare($share);
723
724
		$this->generalCreateChecks($share);
725
726
		// Verify if there are any issues with the path
727
		$this->pathCreateChecks($share->getNode());
728
729
		/*
730
		 * On creation of a share the owner is always the owner of the path
731
		 * Except for mounted federated shares.
732
		 */
733
		$storage = $share->getNode()->getStorage();
734
		if ($storage->instanceOfStorage('OCA\Files_Sharing\External\Storage')) {
735
			$parent = $share->getNode()->getParent();
736
			while ($parent->getStorage()->instanceOfStorage('OCA\Files_Sharing\External\Storage')) {
737
				$parent = $parent->getParent();
738
			}
739
			$share->setShareOwner($parent->getOwner()->getUID());
740
		} else {
741
			if ($share->getNode()->getOwner()) {
742
				$share->setShareOwner($share->getNode()->getOwner()->getUID());
743
			} else {
744
				$share->setShareOwner($share->getSharedBy());
745
			}
746
		}
747
748
		try {
749
			// Verify share type
750
			if ($share->getShareType() === IShare::TYPE_USER) {
751
				$this->userCreateChecks($share);
752
753
				// Verify the expiration date
754
				$share = $this->validateExpirationDateInternal($share);
755
			} elseif ($share->getShareType() === IShare::TYPE_GROUP) {
756
				$this->groupCreateChecks($share);
757
758
				// Verify the expiration date
759
				$share = $this->validateExpirationDateInternal($share);
760
			} elseif ($share->getShareType() === IShare::TYPE_REMOTE || $share->getShareType() === IShare::TYPE_REMOTE_GROUP) {
761
				//Verify the expiration date
762
				$share = $this->validateExpirationDateInternal($share);
763
			} elseif ($share->getShareType() === IShare::TYPE_LINK
764
				|| $share->getShareType() === IShare::TYPE_EMAIL) {
765
				$this->linkCreateChecks($share);
766
				$this->setLinkParent($share);
767
768
				// For now ignore a set token.
769
				$share->setToken(
770
					$this->secureRandom->generate(
771
						\OC\Share\Constants::TOKEN_LENGTH,
772
						\OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE
773
					)
774
				);
775
776
				// Verify the expiration date
777
				$share = $this->validateExpirationDateLink($share);
778
779
				// Verify the password
780
				$this->verifyPassword($share->getPassword());
781
782
				// If a password is set. Hash it!
783
				if ($share->getShareType() === IShare::TYPE_LINK
784
					&& $share->getPassword() !== null) {
785
					$share->setPassword($this->hasher->hash($share->getPassword()));
786
				}
787
			}
788
789
			// Cannot share with the owner
790
			if ($share->getShareType() === IShare::TYPE_USER &&
791
				$share->getSharedWith() === $share->getShareOwner()) {
792
				throw new \InvalidArgumentException('Cannot share with the share owner');
793
			}
794
795
			// Generate the target
796
			$defaultShareFolder = $this->config->getSystemValue('share_folder', '/');
797
			$allowCustomShareFolder = $this->config->getSystemValueBool('sharing.allow_custom_share_folder', true);
798
			if ($allowCustomShareFolder) {
799
				$shareFolder = $this->config->getUserValue($share->getSharedWith(), Application::APP_ID, 'share_folder', $defaultShareFolder);
800
			} else {
801
				$shareFolder = $defaultShareFolder;
802
			}
803
804
			$target = $shareFolder . '/' . $share->getNode()->getName();
805
			$target = \OC\Files\Filesystem::normalizePath($target);
806
			$share->setTarget($target);
807
808
			// Pre share event
809
			$event = new GenericEvent($share);
810
			$this->legacyDispatcher->dispatch('OCP\Share::preShare', $event);
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with $event. ( Ignorable by Annotation )

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

810
			$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
811
                            dispatch('OCP\Share::preShare', $event);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
'OCP\Share::preShare' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

810
			$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ 'OCP\Share::preShare', $event);
Loading history...
811
			if ($event->isPropagationStopped() && $event->hasArgument('error')) {
812
				throw new \Exception($event->getArgument('error'));
813
			}
814
815
			$oldShare = $share;
816
			$provider = $this->factory->getProviderForType($share->getShareType());
817
			$share = $provider->create($share);
818
819
			// Reuse the node we already have
820
			$share->setNode($oldShare->getNode());
821
822
			// Reset the target if it is null for the new share
823
			if ($share->getTarget() === '') {
824
				$share->setTarget($target);
825
			}
826
		} catch (AlreadySharedException $e) {
827
			// if a share for the same target already exists, dont create a new one, but do trigger the hooks and notifications again
828
			$oldShare = $share;
829
830
			// Reuse the node we already have
831
			$share = $e->getExistingShare();
832
			$share->setNode($oldShare->getNode());
833
		}
834
835
		// Post share event
836
		$event = new GenericEvent($share);
837
		$this->legacyDispatcher->dispatch('OCP\Share::postShare', $event);
838
839
		$this->dispatcher->dispatchTyped(new Share\Events\ShareCreatedEvent($share));
840
841
		if ($this->config->getSystemValueBool('sharing.enable_share_mail', true)
842
			&& $share->getShareType() === IShare::TYPE_USER) {
843
			$mailSend = $share->getMailSend();
844
			if ($mailSend === true) {
845
				$user = $this->userManager->get($share->getSharedWith());
846
				if ($user !== null) {
847
					$emailAddress = $user->getEMailAddress();
848
					if ($emailAddress !== null && $emailAddress !== '') {
849
						$userLang = $this->l10nFactory->getUserLanguage($user);
850
						$l = $this->l10nFactory->get('lib', $userLang);
851
						$this->sendMailNotification(
852
							$l,
853
							$share->getNode()->getName(),
854
							$this->urlGenerator->linkToRouteAbsolute('files_sharing.Accept.accept', ['shareId' => $share->getFullId()]),
855
							$share->getSharedBy(),
856
							$emailAddress,
857
							$share->getExpirationDate(),
858
							$share->getNote()
859
						);
860
						$this->logger->debug('Sent share notification to ' . $emailAddress . ' for share with ID ' . $share->getId(), ['app' => 'share']);
861
					} else {
862
						$this->logger->debug('Share notification not sent to ' . $share->getSharedWith() . ' because email address is not set.', ['app' => 'share']);
863
					}
864
				} else {
865
					$this->logger->debug('Share notification not sent to ' . $share->getSharedWith() . ' because user could not be found.', ['app' => 'share']);
866
				}
867
			} else {
868
				$this->logger->debug('Share notification not sent because mailsend is false.', ['app' => 'share']);
869
			}
870
		}
871
872
		return $share;
873
	}
874
875
	/**
876
	 * Send mail notifications
877
	 *
878
	 * This method will catch and log mail transmission errors
879
	 *
880
	 * @param IL10N $l Language of the recipient
881
	 * @param string $filename file/folder name
882
	 * @param string $link link to the file/folder
883
	 * @param string $initiator user ID of share sender
884
	 * @param string $shareWith email address of share receiver
885
	 * @param \DateTime|null $expiration
886
	 */
887
	protected function sendMailNotification(IL10N $l,
888
											$filename,
889
											$link,
890
											$initiator,
891
											$shareWith,
892
											\DateTime $expiration = null,
893
											$note = '') {
894
		$initiatorUser = $this->userManager->get($initiator);
895
		$initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator;
896
897
		$message = $this->mailer->createMessage();
898
899
		$emailTemplate = $this->mailer->createEMailTemplate('files_sharing.RecipientNotification', [
900
			'filename' => $filename,
901
			'link' => $link,
902
			'initiator' => $initiatorDisplayName,
903
			'expiration' => $expiration,
904
			'shareWith' => $shareWith,
905
		]);
906
907
		$emailTemplate->setSubject($l->t('%1$s shared »%2$s« with you', [$initiatorDisplayName, $filename]));
908
		$emailTemplate->addHeader();
909
		$emailTemplate->addHeading($l->t('%1$s shared »%2$s« with you', [$initiatorDisplayName, $filename]), false);
910
		$text = $l->t('%1$s shared »%2$s« with you.', [$initiatorDisplayName, $filename]);
911
912
		if ($note !== '') {
913
			$emailTemplate->addBodyText(htmlspecialchars($note), $note);
914
		}
915
916
		$emailTemplate->addBodyText(
917
			htmlspecialchars($text . ' ' . $l->t('Click the button below to open it.')),
918
			$text
919
		);
920
		$emailTemplate->addBodyButton(
921
			$l->t('Open »%s«', [$filename]),
922
			$link
923
		);
924
925
		$message->setTo([$shareWith]);
926
927
		// The "From" contains the sharers name
928
		$instanceName = $this->defaults->getName();
929
		$senderName = $l->t(
930
			'%1$s via %2$s',
931
			[
932
				$initiatorDisplayName,
933
				$instanceName,
934
			]
935
		);
936
		$message->setFrom([\OCP\Util::getDefaultEmailAddress('noreply') => $senderName]);
937
938
		// The "Reply-To" is set to the sharer if an mail address is configured
939
		// also the default footer contains a "Do not reply" which needs to be adjusted.
940
		$initiatorEmail = $initiatorUser->getEMailAddress();
941
		if ($initiatorEmail !== null) {
942
			$message->setReplyTo([$initiatorEmail => $initiatorDisplayName]);
943
			$emailTemplate->addFooter($instanceName . ($this->defaults->getSlogan($l->getLanguageCode()) !== '' ? ' - ' . $this->defaults->getSlogan($l->getLanguageCode()) : ''));
944
		} else {
945
			$emailTemplate->addFooter('', $l->getLanguageCode());
946
		}
947
948
		$message->useTemplate($emailTemplate);
949
		try {
950
			$failedRecipients = $this->mailer->send($message);
951
			if (!empty($failedRecipients)) {
952
				$this->logger->error('Share notification mail could not be sent to: ' . implode(', ', $failedRecipients));
953
				return;
954
			}
955
		} catch (\Exception $e) {
956
			$this->logger->error('Share notification mail could not be sent', ['exception' => $e]);
957
		}
958
	}
959
960
	/**
961
	 * Update a share
962
	 *
963
	 * @param IShare $share
964
	 * @return IShare The share object
965
	 * @throws \InvalidArgumentException
966
	 */
967
	public function updateShare(IShare $share) {
968
		$expirationDateUpdated = false;
969
970
		$this->canShare($share);
971
972
		try {
973
			$originalShare = $this->getShareById($share->getFullId());
974
		} catch (\UnexpectedValueException $e) {
975
			throw new \InvalidArgumentException('Share does not have a full id');
976
		}
977
978
		// We cannot change the share type!
979
		if ($share->getShareType() !== $originalShare->getShareType()) {
980
			throw new \InvalidArgumentException('Cannot change share type');
981
		}
982
983
		// We can only change the recipient on user shares
984
		if ($share->getSharedWith() !== $originalShare->getSharedWith() &&
985
			$share->getShareType() !== IShare::TYPE_USER) {
986
			throw new \InvalidArgumentException('Can only update recipient on user shares');
987
		}
988
989
		// Cannot share with the owner
990
		if ($share->getShareType() === IShare::TYPE_USER &&
991
			$share->getSharedWith() === $share->getShareOwner()) {
992
			throw new \InvalidArgumentException('Cannot share with the share owner');
993
		}
994
995
		$this->generalCreateChecks($share);
996
997
		if ($share->getShareType() === IShare::TYPE_USER) {
998
			$this->userCreateChecks($share);
999
1000
			if ($share->getExpirationDate() != $originalShare->getExpirationDate()) {
1001
				//Verify the expiration date
1002
				$this->validateExpirationDateInternal($share);
1003
				$expirationDateUpdated = true;
1004
			}
1005
		} elseif ($share->getShareType() === IShare::TYPE_GROUP) {
1006
			$this->groupCreateChecks($share);
1007
1008
			if ($share->getExpirationDate() != $originalShare->getExpirationDate()) {
1009
				//Verify the expiration date
1010
				$this->validateExpirationDateInternal($share);
1011
				$expirationDateUpdated = true;
1012
			}
1013
		} elseif ($share->getShareType() === IShare::TYPE_LINK
1014
			|| $share->getShareType() === IShare::TYPE_EMAIL) {
1015
			$this->linkCreateChecks($share);
1016
1017
			// The new password is not set again if it is the same as the old
1018
			// one, unless when switching from sending by Talk to sending by
1019
			// mail.
1020
			$plainTextPassword = $share->getPassword();
1021
			$updatedPassword = $this->updateSharePasswordIfNeeded($share, $originalShare);
1022
1023
			/**
1024
			 * Cannot enable the getSendPasswordByTalk if there is no password set
1025
			 */
1026
			if (empty($plainTextPassword) && $share->getSendPasswordByTalk()) {
1027
				throw new \InvalidArgumentException('Cannot enable sending the password by Talk with an empty password');
1028
			}
1029
1030
			/**
1031
			 * If we're in a mail share, we need to force a password change
1032
			 * as either the user is not aware of the password or is already (received by mail)
1033
			 * Thus the SendPasswordByTalk feature would not make sense
1034
			 */
1035
			if (!$updatedPassword && $share->getShareType() === IShare::TYPE_EMAIL) {
1036
				if (!$originalShare->getSendPasswordByTalk() && $share->getSendPasswordByTalk()) {
1037
					throw new \InvalidArgumentException('Cannot enable sending the password by Talk without setting a new password');
1038
				}
1039
				if ($originalShare->getSendPasswordByTalk() && !$share->getSendPasswordByTalk()) {
1040
					throw new \InvalidArgumentException('Cannot disable sending the password by Talk without setting a new password');
1041
				}
1042
			}
1043
1044
			if ($share->getExpirationDate() != $originalShare->getExpirationDate()) {
1045
				// Verify the expiration date
1046
				$this->validateExpirationDateLink($share);
1047
				$expirationDateUpdated = true;
1048
			}
1049
		} elseif ($share->getShareType() === IShare::TYPE_REMOTE || $share->getShareType() === IShare::TYPE_REMOTE_GROUP) {
1050
			if ($share->getExpirationDate() != $originalShare->getExpirationDate()) {
1051
				//Verify the expiration date
1052
				$this->validateExpirationDateInternal($share);
1053
				$expirationDateUpdated = true;
1054
			}
1055
		}
1056
1057
		$this->pathCreateChecks($share->getNode());
1058
1059
		// Now update the share!
1060
		$provider = $this->factory->getProviderForType($share->getShareType());
1061
		if ($share->getShareType() === IShare::TYPE_EMAIL) {
1062
			$share = $provider->update($share, $plainTextPassword);
0 ignored issues
show
Unused Code introduced by
The call to OCP\Share\IShareProvider::update() has too many arguments starting with $plainTextPassword. ( Ignorable by Annotation )

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

1062
			/** @scrutinizer ignore-call */ 
1063
   $share = $provider->update($share, $plainTextPassword);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Comprehensibility Best Practice introduced by
The variable $plainTextPassword does not seem to be defined for all execution paths leading up to this point.
Loading history...
1063
		} else {
1064
			$share = $provider->update($share);
1065
		}
1066
1067
		if ($expirationDateUpdated === true) {
1068
			\OC_Hook::emit(Share::class, 'post_set_expiration_date', [
1069
				'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
1070
				'itemSource' => $share->getNode()->getId(),
1071
				'date' => $share->getExpirationDate(),
1072
				'uidOwner' => $share->getSharedBy(),
1073
			]);
1074
		}
1075
1076
		if ($share->getPassword() !== $originalShare->getPassword()) {
1077
			\OC_Hook::emit(Share::class, 'post_update_password', [
1078
				'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
1079
				'itemSource' => $share->getNode()->getId(),
1080
				'uidOwner' => $share->getSharedBy(),
1081
				'token' => $share->getToken(),
1082
				'disabled' => is_null($share->getPassword()),
1083
			]);
1084
		}
1085
1086
		if ($share->getPermissions() !== $originalShare->getPermissions()) {
1087
			if ($this->userManager->userExists($share->getShareOwner())) {
1088
				$userFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
1089
			} else {
1090
				$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
1091
			}
1092
			\OC_Hook::emit(Share::class, 'post_update_permissions', [
1093
				'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
1094
				'itemSource' => $share->getNode()->getId(),
1095
				'shareType' => $share->getShareType(),
1096
				'shareWith' => $share->getSharedWith(),
1097
				'uidOwner' => $share->getSharedBy(),
1098
				'permissions' => $share->getPermissions(),
1099
				'attributes' => $share->getAttributes() !== null ? $share->getAttributes()->toArray() : null,
1100
				'path' => $userFolder->getRelativePath($share->getNode()->getPath()),
1101
			]);
1102
		}
1103
1104
		return $share;
1105
	}
1106
1107
	/**
1108
	 * Accept a share.
1109
	 *
1110
	 * @param IShare $share
1111
	 * @param string $recipientId
1112
	 * @return IShare The share object
1113
	 * @throws \InvalidArgumentException
1114
	 * @since 9.0.0
1115
	 */
1116
	public function acceptShare(IShare $share, string $recipientId): IShare {
1117
		[$providerId,] = $this->splitFullId($share->getFullId());
1118
		$provider = $this->factory->getProvider($providerId);
1119
1120
		if (!method_exists($provider, 'acceptShare')) {
1121
			// TODO FIX ME
1122
			throw new \InvalidArgumentException('Share provider does not support accepting');
1123
		}
1124
		$provider->acceptShare($share, $recipientId);
1125
		$event = new GenericEvent($share);
1126
		$this->legacyDispatcher->dispatch('OCP\Share::postAcceptShare', $event);
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with $event. ( Ignorable by Annotation )

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

1126
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
1127
                           dispatch('OCP\Share::postAcceptShare', $event);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
'OCP\Share::postAcceptShare' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

1126
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ 'OCP\Share::postAcceptShare', $event);
Loading history...
1127
1128
		return $share;
1129
	}
1130
1131
	/**
1132
	 * Updates the password of the given share if it is not the same as the
1133
	 * password of the original share.
1134
	 *
1135
	 * @param IShare $share the share to update its password.
1136
	 * @param IShare $originalShare the original share to compare its
1137
	 *        password with.
1138
	 * @return boolean whether the password was updated or not.
1139
	 */
1140
	private function updateSharePasswordIfNeeded(IShare $share, IShare $originalShare) {
1141
		$passwordsAreDifferent = ($share->getPassword() !== $originalShare->getPassword()) &&
1142
			(($share->getPassword() !== null && $originalShare->getPassword() === null) ||
0 ignored issues
show
introduced by
The condition $originalShare->getPassword() === null is always false.
Loading history...
1143
				($share->getPassword() === null && $originalShare->getPassword() !== null) ||
0 ignored issues
show
introduced by
The condition $share->getPassword() === null is always false.
Loading history...
1144
				($share->getPassword() !== null && $originalShare->getPassword() !== null &&
1145
					!$this->hasher->verify($share->getPassword(), $originalShare->getPassword())));
1146
1147
		// Password updated.
1148
		if ($passwordsAreDifferent) {
1149
			//Verify the password
1150
			$this->verifyPassword($share->getPassword());
1151
1152
			// If a password is set. Hash it!
1153
			if (!empty($share->getPassword())) {
1154
				$share->setPassword($this->hasher->hash($share->getPassword()));
1155
				if ($share->getShareType() === IShare::TYPE_EMAIL) {
1156
					// Shares shared by email have temporary passwords
1157
					$this->setSharePasswordExpirationTime($share);
1158
				}
1159
1160
				return true;
1161
			} else {
1162
				// Empty string and null are seen as NOT password protected
1163
				$share->setPassword(null);
1164
				if ($share->getShareType() === IShare::TYPE_EMAIL) {
1165
					$share->setPasswordExpirationTime(null);
1166
				}
1167
				return true;
1168
			}
1169
		} else {
1170
			// Reset the password to the original one, as it is either the same
1171
			// as the "new" password or a hashed version of it.
1172
			$share->setPassword($originalShare->getPassword());
1173
		}
1174
1175
		return false;
1176
	}
1177
1178
	/**
1179
	 * Set the share's password expiration time
1180
	 */
1181
	private function setSharePasswordExpirationTime(IShare $share): void {
1182
		if (!$this->config->getSystemValueBool('sharing.enable_mail_link_password_expiration', false)) {
1183
			// Sets password expiration date to NULL
1184
			$share->setPasswordExpirationTime();
1185
			return;
1186
		}
1187
		// Sets password expiration date
1188
		$expirationTime = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $expirationTime is dead and can be removed.
Loading history...
1189
		$now = new \DateTime();
1190
		$expirationInterval = $this->config->getSystemValue('sharing.mail_link_password_expiration_interval', 3600);
1191
		$expirationTime = $now->add(new \DateInterval('PT' . $expirationInterval . 'S'));
1192
		$share->setPasswordExpirationTime($expirationTime);
1193
	}
1194
1195
1196
	/**
1197
	 * Delete all the children of this share
1198
	 * FIXME: remove once https://github.com/owncloud/core/pull/21660 is in
1199
	 *
1200
	 * @param IShare $share
1201
	 * @return IShare[] List of deleted shares
1202
	 */
1203
	protected function deleteChildren(IShare $share) {
1204
		$deletedShares = [];
1205
1206
		$provider = $this->factory->getProviderForType($share->getShareType());
1207
1208
		foreach ($provider->getChildren($share) as $child) {
0 ignored issues
show
Bug introduced by
The method getChildren() does not exist on OCP\Share\IShareProvider. Since it exists in all sub-types, consider adding an abstract or default implementation to OCP\Share\IShareProvider. ( Ignorable by Annotation )

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

1208
		foreach ($provider->/** @scrutinizer ignore-call */ getChildren($share) as $child) {
Loading history...
1209
			$deletedChildren = $this->deleteChildren($child);
1210
			$deletedShares = array_merge($deletedShares, $deletedChildren);
1211
1212
			$provider->delete($child);
1213
			$this->dispatcher->dispatchTyped(new Share\Events\ShareDeletedEvent($child));
1214
			$deletedShares[] = $child;
1215
		}
1216
1217
		return $deletedShares;
1218
	}
1219
1220
	/**
1221
	 * Delete a share
1222
	 *
1223
	 * @param IShare $share
1224
	 * @throws ShareNotFound
1225
	 * @throws \InvalidArgumentException
1226
	 */
1227
	public function deleteShare(IShare $share) {
1228
		try {
1229
			$share->getFullId();
1230
		} catch (\UnexpectedValueException $e) {
1231
			throw new \InvalidArgumentException('Share does not have a full id');
1232
		}
1233
1234
		$event = new GenericEvent($share);
1235
		$this->legacyDispatcher->dispatch('OCP\Share::preUnshare', $event);
0 ignored issues
show
Bug introduced by
'OCP\Share::preUnshare' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

1235
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ 'OCP\Share::preUnshare', $event);
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with $event. ( Ignorable by Annotation )

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

1235
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
1236
                           dispatch('OCP\Share::preUnshare', $event);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1236
1237
		// Get all children and delete them as well
1238
		$deletedShares = $this->deleteChildren($share);
1239
1240
		// Do the actual delete
1241
		$provider = $this->factory->getProviderForType($share->getShareType());
1242
		$provider->delete($share);
1243
1244
		$this->dispatcher->dispatchTyped(new Share\Events\ShareDeletedEvent($share));
1245
1246
		// All the deleted shares caused by this delete
1247
		$deletedShares[] = $share;
1248
1249
		// Emit post hook
1250
		$event->setArgument('deletedShares', $deletedShares);
1251
		$this->legacyDispatcher->dispatch('OCP\Share::postUnshare', $event);
1252
	}
1253
1254
1255
	/**
1256
	 * Unshare a file as the recipient.
1257
	 * This can be different from a regular delete for example when one of
1258
	 * the users in a groups deletes that share. But the provider should
1259
	 * handle this.
1260
	 *
1261
	 * @param IShare $share
1262
	 * @param string $recipientId
1263
	 */
1264
	public function deleteFromSelf(IShare $share, $recipientId) {
1265
		[$providerId,] = $this->splitFullId($share->getFullId());
1266
		$provider = $this->factory->getProvider($providerId);
1267
1268
		$provider->deleteFromSelf($share, $recipientId);
1269
		$event = new GenericEvent($share);
1270
		$this->legacyDispatcher->dispatch('OCP\Share::postUnshareFromSelf', $event);
0 ignored issues
show
Bug introduced by
'OCP\Share::postUnshareFromSelf' of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

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

1270
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ 'OCP\Share::postUnshareFromSelf', $event);
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with $event. ( Ignorable by Annotation )

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

1270
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
1271
                           dispatch('OCP\Share::postUnshareFromSelf', $event);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1271
	}
1272
1273
	public function restoreShare(IShare $share, string $recipientId): IShare {
1274
		[$providerId,] = $this->splitFullId($share->getFullId());
1275
		$provider = $this->factory->getProvider($providerId);
1276
1277
		return $provider->restore($share, $recipientId);
1278
	}
1279
1280
	/**
1281
	 * @inheritdoc
1282
	 */
1283
	public function moveShare(IShare $share, $recipientId) {
1284
		if ($share->getShareType() === IShare::TYPE_LINK
1285
			|| $share->getShareType() === IShare::TYPE_EMAIL) {
1286
			throw new \InvalidArgumentException('Cannot change target of link share');
1287
		}
1288
1289
		if ($share->getShareType() === IShare::TYPE_USER && $share->getSharedWith() !== $recipientId) {
1290
			throw new \InvalidArgumentException('Invalid recipient');
1291
		}
1292
1293
		if ($share->getShareType() === IShare::TYPE_GROUP) {
1294
			$sharedWith = $this->groupManager->get($share->getSharedWith());
1295
			if (is_null($sharedWith)) {
1296
				throw new \InvalidArgumentException('Group "' . $share->getSharedWith() . '" does not exist');
1297
			}
1298
			$recipient = $this->userManager->get($recipientId);
1299
			if (!$sharedWith->inGroup($recipient)) {
1300
				throw new \InvalidArgumentException('Invalid recipient');
1301
			}
1302
		}
1303
1304
		[$providerId,] = $this->splitFullId($share->getFullId());
1305
		$provider = $this->factory->getProvider($providerId);
1306
1307
		return $provider->move($share, $recipientId);
1308
	}
1309
1310
	public function getSharesInFolder($userId, Folder $node, $reshares = false, $shallow = true) {
1311
		$providers = $this->factory->getAllProviders();
1312
1313
		return array_reduce($providers, function ($shares, IShareProvider $provider) use ($userId, $node, $reshares, $shallow) {
1314
			$newShares = $provider->getSharesInFolder($userId, $node, $reshares, $shallow);
1315
			foreach ($newShares as $fid => $data) {
1316
				if (!isset($shares[$fid])) {
1317
					$shares[$fid] = [];
1318
				}
1319
1320
				$shares[$fid] = array_merge($shares[$fid], $data);
1321
			}
1322
			return $shares;
1323
		}, []);
1324
	}
1325
1326
	/**
1327
	 * @inheritdoc
1328
	 */
1329
	public function getSharesBy($userId, $shareType, $path = null, $reshares = false, $limit = 50, $offset = 0) {
1330
		if ($path !== null &&
1331
			!($path instanceof \OCP\Files\File) &&
1332
			!($path instanceof \OCP\Files\Folder)) {
1333
			throw new \InvalidArgumentException('invalid path');
1334
		}
1335
1336
		try {
1337
			$provider = $this->factory->getProviderForType($shareType);
1338
		} catch (ProviderException $e) {
1339
			return [];
1340
		}
1341
1342
		$shares = $provider->getSharesBy($userId, $shareType, $path, $reshares, $limit, $offset);
1343
1344
		/*
1345
		 * Work around so we don't return expired shares but still follow
1346
		 * proper pagination.
1347
		 */
1348
1349
		$shares2 = [];
1350
1351
		while (true) {
1352
			$added = 0;
1353
			foreach ($shares as $share) {
1354
				try {
1355
					$this->checkExpireDate($share);
1356
				} catch (ShareNotFound $e) {
1357
					//Ignore since this basically means the share is deleted
1358
					continue;
1359
				}
1360
1361
				$added++;
1362
				$shares2[] = $share;
1363
1364
				if (count($shares2) === $limit) {
1365
					break;
1366
				}
1367
			}
1368
1369
			// If we did not fetch more shares than the limit then there are no more shares
1370
			if (count($shares) < $limit) {
1371
				break;
1372
			}
1373
1374
			if (count($shares2) === $limit) {
1375
				break;
1376
			}
1377
1378
			// If there was no limit on the select we are done
1379
			if ($limit === -1) {
1380
				break;
1381
			}
1382
1383
			$offset += $added;
1384
1385
			// Fetch again $limit shares
1386
			$shares = $provider->getSharesBy($userId, $shareType, $path, $reshares, $limit, $offset);
1387
1388
			// No more shares means we are done
1389
			if (empty($shares)) {
1390
				break;
1391
			}
1392
		}
1393
1394
		$shares = $shares2;
1395
1396
		return $shares;
1397
	}
1398
1399
	/**
1400
	 * @inheritdoc
1401
	 */
1402
	public function getSharedWith($userId, $shareType, $node = null, $limit = 50, $offset = 0) {
1403
		try {
1404
			$provider = $this->factory->getProviderForType($shareType);
1405
		} catch (ProviderException $e) {
1406
			return [];
1407
		}
1408
1409
		$shares = $provider->getSharedWith($userId, $shareType, $node, $limit, $offset);
1410
1411
		// remove all shares which are already expired
1412
		foreach ($shares as $key => $share) {
1413
			try {
1414
				$this->checkExpireDate($share);
1415
			} catch (ShareNotFound $e) {
1416
				unset($shares[$key]);
1417
			}
1418
		}
1419
1420
		return $shares;
1421
	}
1422
1423
	/**
1424
	 * @inheritdoc
1425
	 */
1426
	public function getDeletedSharedWith($userId, $shareType, $node = null, $limit = 50, $offset = 0) {
1427
		$shares = $this->getSharedWith($userId, $shareType, $node, $limit, $offset);
1428
1429
		// Only get deleted shares
1430
		$shares = array_filter($shares, function (IShare $share) {
1431
			return $share->getPermissions() === 0;
1432
		});
1433
1434
		// Only get shares where the owner still exists
1435
		$shares = array_filter($shares, function (IShare $share) {
1436
			return $this->userManager->userExists($share->getShareOwner());
1437
		});
1438
1439
		return $shares;
1440
	}
1441
1442
	/**
1443
	 * @inheritdoc
1444
	 */
1445
	public function getShareById($id, $recipient = null) {
1446
		if ($id === null) {
1447
			throw new ShareNotFound();
1448
		}
1449
1450
		[$providerId, $id] = $this->splitFullId($id);
1451
1452
		try {
1453
			$provider = $this->factory->getProvider($providerId);
1454
		} catch (ProviderException $e) {
1455
			throw new ShareNotFound();
1456
		}
1457
1458
		$share = $provider->getShareById($id, $recipient);
0 ignored issues
show
Bug introduced by
$id of type string is incompatible with the type integer expected by parameter $id of OCP\Share\IShareProvider::getShareById(). ( Ignorable by Annotation )

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

1458
		$share = $provider->getShareById(/** @scrutinizer ignore-type */ $id, $recipient);
Loading history...
1459
1460
		$this->checkExpireDate($share);
1461
1462
		return $share;
1463
	}
1464
1465
	/**
1466
	 * Get all the shares for a given path
1467
	 *
1468
	 * @param \OCP\Files\Node $path
1469
	 * @param int $page
1470
	 * @param int $perPage
1471
	 *
1472
	 * @return Share[]
1473
	 */
1474
	public function getSharesByPath(\OCP\Files\Node $path, $page = 0, $perPage = 50) {
0 ignored issues
show
Unused Code introduced by
The parameter $perPage is not used and could be removed. ( Ignorable by Annotation )

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

1474
	public function getSharesByPath(\OCP\Files\Node $path, $page = 0, /** @scrutinizer ignore-unused */ $perPage = 50) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $path is not used and could be removed. ( Ignorable by Annotation )

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

1474
	public function getSharesByPath(/** @scrutinizer ignore-unused */ \OCP\Files\Node $path, $page = 0, $perPage = 50) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $page is not used and could be removed. ( Ignorable by Annotation )

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

1474
	public function getSharesByPath(\OCP\Files\Node $path, /** @scrutinizer ignore-unused */ $page = 0, $perPage = 50) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1475
		return [];
1476
	}
1477
1478
	/**
1479
	 * Get the share by token possible with password
1480
	 *
1481
	 * @param string $token
1482
	 * @return IShare
1483
	 *
1484
	 * @throws ShareNotFound
1485
	 */
1486
	public function getShareByToken($token) {
1487
		// tokens cannot be valid local user names
1488
		if ($this->userManager->userExists($token)) {
1489
			throw new ShareNotFound();
1490
		}
1491
		$share = null;
1492
		try {
1493
			if ($this->shareApiAllowLinks()) {
1494
				$provider = $this->factory->getProviderForType(IShare::TYPE_LINK);
1495
				$share = $provider->getShareByToken($token);
1496
			}
1497
		} catch (ProviderException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1498
		} catch (ShareNotFound $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1499
		}
1500
1501
1502
		// If it is not a link share try to fetch a federated share by token
1503
		if ($share === null) {
1504
			try {
1505
				$provider = $this->factory->getProviderForType(IShare::TYPE_REMOTE);
1506
				$share = $provider->getShareByToken($token);
1507
			} catch (ProviderException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1508
			} catch (ShareNotFound $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1509
			}
1510
		}
1511
1512
		// If it is not a link share try to fetch a mail share by token
1513
		if ($share === null && $this->shareProviderExists(IShare::TYPE_EMAIL)) {
1514
			try {
1515
				$provider = $this->factory->getProviderForType(IShare::TYPE_EMAIL);
1516
				$share = $provider->getShareByToken($token);
1517
			} catch (ProviderException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1518
			} catch (ShareNotFound $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1519
			}
1520
		}
1521
1522
		if ($share === null && $this->shareProviderExists(IShare::TYPE_CIRCLE)) {
1523
			try {
1524
				$provider = $this->factory->getProviderForType(IShare::TYPE_CIRCLE);
1525
				$share = $provider->getShareByToken($token);
1526
			} catch (ProviderException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1527
			} catch (ShareNotFound $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1528
			}
1529
		}
1530
1531
		if ($share === null && $this->shareProviderExists(IShare::TYPE_ROOM)) {
1532
			try {
1533
				$provider = $this->factory->getProviderForType(IShare::TYPE_ROOM);
1534
				$share = $provider->getShareByToken($token);
1535
			} catch (ProviderException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1536
			} catch (ShareNotFound $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1537
			}
1538
		}
1539
1540
		if ($share === null) {
1541
			throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
1542
		}
1543
1544
		$this->checkExpireDate($share);
1545
1546
		/*
1547
		 * Reduce the permissions for link or email shares if public upload is not enabled
1548
		 */
1549
		if (($share->getShareType() === IShare::TYPE_LINK || $share->getShareType() === IShare::TYPE_EMAIL)
1550
			&& $share->getNodeType() === 'folder' && !$this->shareApiLinkAllowPublicUpload()) {
1551
			$share->setPermissions($share->getPermissions() & ~(\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE));
1552
		}
1553
1554
		return $share;
1555
	}
1556
1557
	protected function checkExpireDate($share) {
1558
		if ($share->isExpired()) {
1559
			$this->deleteShare($share);
1560
			throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
1561
		}
1562
	}
1563
1564
	/**
1565
	 * Verify the password of a public share
1566
	 *
1567
	 * @param IShare $share
1568
	 * @param ?string $password
1569
	 * @return bool
1570
	 */
1571
	public function checkPassword(IShare $share, $password) {
1572
		$passwordProtected = $share->getShareType() !== IShare::TYPE_LINK
1573
			|| $share->getShareType() !== IShare::TYPE_EMAIL
1574
			|| $share->getShareType() !== IShare::TYPE_CIRCLE;
1575
		if (!$passwordProtected) {
1576
			//TODO maybe exception?
1577
			return false;
1578
		}
1579
1580
		if ($password === null || $share->getPassword() === null) {
1581
			return false;
1582
		}
1583
1584
		// Makes sure password hasn't expired
1585
		$expirationTime = $share->getPasswordExpirationTime();
1586
		if ($expirationTime !== null && $expirationTime < new \DateTime()) {
1587
			return false;
1588
		}
1589
1590
		$newHash = '';
1591
		if (!$this->hasher->verify($password, $share->getPassword(), $newHash)) {
1592
			return false;
1593
		}
1594
1595
		if (!empty($newHash)) {
1596
			$share->setPassword($newHash);
1597
			$provider = $this->factory->getProviderForType($share->getShareType());
1598
			$provider->update($share);
1599
		}
1600
1601
		return true;
1602
	}
1603
1604
	/**
1605
	 * @inheritdoc
1606
	 */
1607
	public function userDeleted($uid) {
1608
		$types = [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK, IShare::TYPE_REMOTE, IShare::TYPE_EMAIL];
1609
1610
		foreach ($types as $type) {
1611
			try {
1612
				$provider = $this->factory->getProviderForType($type);
1613
			} catch (ProviderException $e) {
1614
				continue;
1615
			}
1616
			$provider->userDeleted($uid, $type);
1617
		}
1618
	}
1619
1620
	/**
1621
	 * @inheritdoc
1622
	 */
1623
	public function groupDeleted($gid) {
1624
		$provider = $this->factory->getProviderForType(IShare::TYPE_GROUP);
1625
		$provider->groupDeleted($gid);
1626
1627
		$excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', '');
1628
		if ($excludedGroups === '') {
1629
			return;
1630
		}
1631
1632
		$excludedGroups = json_decode($excludedGroups, true);
1633
		if (json_last_error() !== JSON_ERROR_NONE) {
1634
			return;
1635
		}
1636
1637
		$excludedGroups = array_diff($excludedGroups, [$gid]);
1638
		$this->config->setAppValue('core', 'shareapi_exclude_groups_list', json_encode($excludedGroups));
1639
	}
1640
1641
	/**
1642
	 * @inheritdoc
1643
	 */
1644
	public function userDeletedFromGroup($uid, $gid) {
1645
		$provider = $this->factory->getProviderForType(IShare::TYPE_GROUP);
1646
		$provider->userDeletedFromGroup($uid, $gid);
1647
	}
1648
1649
	/**
1650
	 * Get access list to a path. This means
1651
	 * all the users that can access a given path.
1652
	 *
1653
	 * Consider:
1654
	 * -root
1655
	 * |-folder1 (23)
1656
	 *  |-folder2 (32)
1657
	 *   |-fileA (42)
1658
	 *
1659
	 * fileA is shared with user1 and user1@server1
1660
	 * folder2 is shared with group2 (user4 is a member of group2)
1661
	 * folder1 is shared with user2 (renamed to "folder (1)") and user2@server2
1662
	 *
1663
	 * Then the access list to '/folder1/folder2/fileA' with $currentAccess is:
1664
	 * [
1665
	 *  users  => [
1666
	 *      'user1' => ['node_id' => 42, 'node_path' => '/fileA'],
1667
	 *      'user4' => ['node_id' => 32, 'node_path' => '/folder2'],
1668
	 *      'user2' => ['node_id' => 23, 'node_path' => '/folder (1)'],
1669
	 *  ],
1670
	 *  remote => [
1671
	 *      'user1@server1' => ['node_id' => 42, 'token' => 'SeCr3t'],
1672
	 *      'user2@server2' => ['node_id' => 23, 'token' => 'FooBaR'],
1673
	 *  ],
1674
	 *  public => bool
1675
	 *  mail => bool
1676
	 * ]
1677
	 *
1678
	 * The access list to '/folder1/folder2/fileA' **without** $currentAccess is:
1679
	 * [
1680
	 *  users  => ['user1', 'user2', 'user4'],
1681
	 *  remote => bool,
1682
	 *  public => bool
1683
	 *  mail => bool
1684
	 * ]
1685
	 *
1686
	 * This is required for encryption/activity
1687
	 *
1688
	 * @param \OCP\Files\Node $path
1689
	 * @param bool $recursive Should we check all parent folders as well
1690
	 * @param bool $currentAccess Ensure the recipient has access to the file (e.g. did not unshare it)
1691
	 * @return array
1692
	 */
1693
	public function getAccessList(\OCP\Files\Node $path, $recursive = true, $currentAccess = false) {
1694
		$owner = $path->getOwner();
1695
1696
		if ($owner === null) {
1697
			return [];
1698
		}
1699
1700
		$owner = $owner->getUID();
1701
1702
		if ($currentAccess) {
1703
			$al = ['users' => [], 'remote' => [], 'public' => false];
1704
		} else {
1705
			$al = ['users' => [], 'remote' => false, 'public' => false];
1706
		}
1707
		if (!$this->userManager->userExists($owner)) {
1708
			return $al;
1709
		}
1710
1711
		//Get node for the owner and correct the owner in case of external storage
1712
		$userFolder = $this->rootFolder->getUserFolder($owner);
1713
		if ($path->getId() !== $userFolder->getId() && !$userFolder->isSubNode($path)) {
1714
			$nodes = $userFolder->getById($path->getId());
1715
			$path = array_shift($nodes);
1716
			if ($path === null || $path->getOwner() === null) {
1717
				return [];
1718
			}
1719
			$owner = $path->getOwner()->getUID();
1720
		}
1721
1722
		$providers = $this->factory->getAllProviders();
1723
1724
		/** @var Node[] $nodes */
1725
		$nodes = [];
1726
1727
1728
		if ($currentAccess) {
1729
			$ownerPath = $path->getPath();
1730
			$ownerPath = explode('/', $ownerPath, 4);
1731
			if (count($ownerPath) < 4) {
1732
				$ownerPath = '';
1733
			} else {
1734
				$ownerPath = $ownerPath[3];
1735
			}
1736
			$al['users'][$owner] = [
1737
				'node_id' => $path->getId(),
1738
				'node_path' => '/' . $ownerPath,
1739
			];
1740
		} else {
1741
			$al['users'][] = $owner;
1742
		}
1743
1744
		// Collect all the shares
1745
		while ($path->getPath() !== $userFolder->getPath()) {
1746
			$nodes[] = $path;
1747
			if (!$recursive) {
1748
				break;
1749
			}
1750
			$path = $path->getParent();
1751
		}
1752
1753
		foreach ($providers as $provider) {
1754
			$tmp = $provider->getAccessList($nodes, $currentAccess);
1755
1756
			foreach ($tmp as $k => $v) {
1757
				if (isset($al[$k])) {
1758
					if (is_array($al[$k])) {
1759
						if ($currentAccess) {
1760
							$al[$k] += $v;
1761
						} else {
1762
							$al[$k] = array_merge($al[$k], $v);
1763
							$al[$k] = array_unique($al[$k]);
1764
							$al[$k] = array_values($al[$k]);
1765
						}
1766
					} else {
1767
						$al[$k] = $al[$k] || $v;
1768
					}
1769
				} else {
1770
					$al[$k] = $v;
1771
				}
1772
			}
1773
		}
1774
1775
		return $al;
1776
	}
1777
1778
	/**
1779
	 * Create a new share
1780
	 *
1781
	 * @return IShare
1782
	 */
1783
	public function newShare() {
1784
		return new \OC\Share20\Share($this->rootFolder, $this->userManager);
1785
	}
1786
1787
	/**
1788
	 * Is the share API enabled
1789
	 *
1790
	 * @return bool
1791
	 */
1792
	public function shareApiEnabled() {
1793
		return $this->config->getAppValue('core', 'shareapi_enabled', 'yes') === 'yes';
1794
	}
1795
1796
	/**
1797
	 * Is public link sharing enabled
1798
	 *
1799
	 * @return bool
1800
	 */
1801
	public function shareApiAllowLinks() {
1802
		if ($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') {
1803
			return false;
1804
		}
1805
1806
		$user = $this->userSession->getUser();
1807
		if ($user) {
1808
			$excludedGroups = json_decode($this->config->getAppValue('core', 'shareapi_allow_links_exclude_groups', '[]'));
1809
			if ($excludedGroups) {
1810
				$userGroups = $this->groupManager->getUserGroupIds($user);
1811
				return !(bool)array_intersect($excludedGroups, $userGroups);
1812
			}
1813
		}
1814
1815
		return true;
1816
	}
1817
1818
	/**
1819
	 * Is password on public link requires
1820
	 *
1821
	 * @param bool Check group membership exclusion
0 ignored issues
show
Bug introduced by
The type OC\Share20\Check was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1822
	 * @return bool
1823
	 */
1824
	public function shareApiLinkEnforcePassword(bool $checkGroupMembership = true) {
1825
		$excludedGroups = $this->config->getAppValue('core', 'shareapi_enforce_links_password_excluded_groups', '');
1826
		if ($excludedGroups !== '' && $checkGroupMembership) {
1827
			$excludedGroups = json_decode($excludedGroups);
1828
			$user = $this->userSession->getUser();
1829
			if ($user) {
1830
				$userGroups = $this->groupManager->getUserGroupIds($user);
1831
				if ((bool)array_intersect($excludedGroups, $userGroups)) {
1832
					return false;
1833
				}
1834
			}
1835
		}
1836
		return $this->config->getAppValue('core', 'shareapi_enforce_links_password', 'no') === 'yes';
1837
	}
1838
1839
	/**
1840
	 * Is default link expire date enabled
1841
	 *
1842
	 * @return bool
1843
	 */
1844
	public function shareApiLinkDefaultExpireDate() {
1845
		return $this->config->getAppValue('core', 'shareapi_default_expire_date', 'no') === 'yes';
1846
	}
1847
1848
	/**
1849
	 * Is default link expire date enforced
1850
	 *`
1851
	 *
1852
	 * @return bool
1853
	 */
1854
	public function shareApiLinkDefaultExpireDateEnforced() {
1855
		return $this->shareApiLinkDefaultExpireDate() &&
1856
			$this->config->getAppValue('core', 'shareapi_enforce_expire_date', 'no') === 'yes';
1857
	}
1858
1859
1860
	/**
1861
	 * Number of default link expire days
1862
	 *
1863
	 * @return int
1864
	 */
1865
	public function shareApiLinkDefaultExpireDays() {
1866
		return (int)$this->config->getAppValue('core', 'shareapi_expire_after_n_days', '7');
1867
	}
1868
1869
	/**
1870
	 * Is default internal expire date enabled
1871
	 *
1872
	 * @return bool
1873
	 */
1874
	public function shareApiInternalDefaultExpireDate(): bool {
1875
		return $this->config->getAppValue('core', 'shareapi_default_internal_expire_date', 'no') === 'yes';
1876
	}
1877
1878
	/**
1879
	 * Is default remote expire date enabled
1880
	 *
1881
	 * @return bool
1882
	 */
1883
	public function shareApiRemoteDefaultExpireDate(): bool {
1884
		return $this->config->getAppValue('core', 'shareapi_default_remote_expire_date', 'no') === 'yes';
1885
	}
1886
1887
	/**
1888
	 * Is default expire date enforced
1889
	 *
1890
	 * @return bool
1891
	 */
1892
	public function shareApiInternalDefaultExpireDateEnforced(): bool {
1893
		return $this->shareApiInternalDefaultExpireDate() &&
1894
			$this->config->getAppValue('core', 'shareapi_enforce_internal_expire_date', 'no') === 'yes';
1895
	}
1896
1897
	/**
1898
	 * Is default expire date enforced for remote shares
1899
	 *
1900
	 * @return bool
1901
	 */
1902
	public function shareApiRemoteDefaultExpireDateEnforced(): bool {
1903
		return $this->shareApiRemoteDefaultExpireDate() &&
1904
			$this->config->getAppValue('core', 'shareapi_enforce_remote_expire_date', 'no') === 'yes';
1905
	}
1906
1907
	/**
1908
	 * Number of default expire days
1909
	 *
1910
	 * @return int
1911
	 */
1912
	public function shareApiInternalDefaultExpireDays(): int {
1913
		return (int)$this->config->getAppValue('core', 'shareapi_internal_expire_after_n_days', '7');
1914
	}
1915
1916
	/**
1917
	 * Number of default expire days for remote shares
1918
	 *
1919
	 * @return int
1920
	 */
1921
	public function shareApiRemoteDefaultExpireDays(): int {
1922
		return (int)$this->config->getAppValue('core', 'shareapi_remote_expire_after_n_days', '7');
1923
	}
1924
1925
	/**
1926
	 * Allow public upload on link shares
1927
	 *
1928
	 * @return bool
1929
	 */
1930
	public function shareApiLinkAllowPublicUpload() {
1931
		return $this->config->getAppValue('core', 'shareapi_allow_public_upload', 'yes') === 'yes';
1932
	}
1933
1934
	/**
1935
	 * check if user can only share with group members
1936
	 *
1937
	 * @return bool
1938
	 */
1939
	public function shareWithGroupMembersOnly() {
1940
		return $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
1941
	}
1942
1943
	/**
1944
	 * Check if users can share with groups
1945
	 *
1946
	 * @return bool
1947
	 */
1948
	public function allowGroupSharing() {
1949
		return $this->config->getAppValue('core', 'shareapi_allow_group_sharing', 'yes') === 'yes';
1950
	}
1951
1952
	public function allowEnumeration(): bool {
1953
		return $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
1954
	}
1955
1956
	public function limitEnumerationToGroups(): bool {
1957
		return $this->allowEnumeration() &&
1958
			$this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
1959
	}
1960
1961
	public function limitEnumerationToPhone(): bool {
1962
		return $this->allowEnumeration() &&
1963
			$this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
1964
	}
1965
1966
	public function allowEnumerationFullMatch(): bool {
1967
		return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes';
1968
	}
1969
1970
	public function matchEmail(): bool {
1971
		return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes';
1972
	}
1973
1974
	public function ignoreSecondDisplayName(): bool {
1975
		return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_dn', 'no') === 'yes';
1976
	}
1977
1978
	public function currentUserCanEnumerateTargetUser(?IUser $currentUser, IUser $targetUser): bool {
1979
		if ($this->allowEnumerationFullMatch()) {
1980
			return true;
1981
		}
1982
1983
		if (!$this->allowEnumeration()) {
1984
			return false;
1985
		}
1986
1987
		if (!$this->limitEnumerationToPhone() && !$this->limitEnumerationToGroups()) {
1988
			// Enumeration is enabled and not restricted: OK
1989
			return true;
1990
		}
1991
1992
		if (!$currentUser instanceof IUser) {
1993
			// Enumeration restrictions require an account
1994
			return false;
1995
		}
1996
1997
		// Enumeration is limited to phone match
1998
		if ($this->limitEnumerationToPhone() && $this->knownUserService->isKnownToUser($currentUser->getUID(), $targetUser->getUID())) {
1999
			return true;
2000
		}
2001
2002
		// Enumeration is limited to groups
2003
		if ($this->limitEnumerationToGroups()) {
2004
			$currentUserGroupIds = $this->groupManager->getUserGroupIds($currentUser);
2005
			$targetUserGroupIds = $this->groupManager->getUserGroupIds($targetUser);
2006
			if (!empty(array_intersect($currentUserGroupIds, $targetUserGroupIds))) {
2007
				return true;
2008
			}
2009
		}
2010
2011
		return false;
2012
	}
2013
2014
	/**
2015
	 * Copied from \OC_Util::isSharingDisabledForUser
2016
	 *
2017
	 * TODO: Deprecate function from OC_Util
2018
	 *
2019
	 * @param string $userId
2020
	 * @return bool
2021
	 */
2022
	public function sharingDisabledForUser($userId) {
2023
		if ($userId === null) {
0 ignored issues
show
introduced by
The condition $userId === null is always false.
Loading history...
2024
			return false;
2025
		}
2026
2027
		if (isset($this->sharingDisabledForUsersCache[$userId])) {
2028
			return $this->sharingDisabledForUsersCache[$userId];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->sharingDis...dForUsersCache[$userId] returns the type OCP\Cache\T which is incompatible with the documented return type boolean.
Loading history...
2029
		}
2030
2031
		if ($this->config->getAppValue('core', 'shareapi_exclude_groups', 'no') === 'yes') {
2032
			$groupsList = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', '');
2033
			$excludedGroups = json_decode($groupsList);
2034
			if (is_null($excludedGroups)) {
2035
				$excludedGroups = explode(',', $groupsList);
2036
				$newValue = json_encode($excludedGroups);
2037
				$this->config->setAppValue('core', 'shareapi_exclude_groups_list', $newValue);
2038
			}
2039
			$user = $this->userManager->get($userId);
2040
			$usersGroups = $this->groupManager->getUserGroupIds($user);
2041
			if (!empty($usersGroups)) {
2042
				$remainingGroups = array_diff($usersGroups, $excludedGroups);
2043
				// if the user is only in groups which are disabled for sharing then
2044
				// sharing is also disabled for the user
2045
				if (empty($remainingGroups)) {
2046
					$this->sharingDisabledForUsersCache[$userId] = true;
2047
					return true;
2048
				}
2049
			}
2050
		}
2051
2052
		$this->sharingDisabledForUsersCache[$userId] = false;
2053
		return false;
2054
	}
2055
2056
	/**
2057
	 * @inheritdoc
2058
	 */
2059
	public function outgoingServer2ServerSharesAllowed() {
2060
		return $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'yes';
2061
	}
2062
2063
	/**
2064
	 * @inheritdoc
2065
	 */
2066
	public function outgoingServer2ServerGroupSharesAllowed() {
2067
		return $this->config->getAppValue('files_sharing', 'outgoing_server2server_group_share_enabled', 'no') === 'yes';
2068
	}
2069
2070
	/**
2071
	 * @inheritdoc
2072
	 */
2073
	public function shareProviderExists($shareType) {
2074
		try {
2075
			$this->factory->getProviderForType($shareType);
2076
		} catch (ProviderException $e) {
2077
			return false;
2078
		}
2079
2080
		return true;
2081
	}
2082
2083
	public function registerShareProvider(string $shareProviderClass): void {
2084
		$this->factory->registerProvider($shareProviderClass);
2085
	}
2086
2087
	public function getAllShares(): iterable {
2088
		$providers = $this->factory->getAllProviders();
2089
2090
		foreach ($providers as $provider) {
2091
			yield from $provider->getAllShares();
2092
		}
2093
	}
2094
}
2095