Passed
Push — master ( 8ef920...1c35b3 )
by Julius
32:28 queued 12s
created

Manager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 36
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
c 0
b 0
f 0
dl 0
loc 36
rs 9.6666
cc 1
nc 1
nop 16

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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æ (skjnldsv) <[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 Vincent Petry <[email protected]>
23
 *
24
 * @license AGPL-3.0
25
 *
26
 * This code is free software: you can redistribute it and/or modify
27
 * it under the terms of the GNU Affero General Public License, version 3,
28
 * as published by the Free Software Foundation.
29
 *
30
 * This program is distributed in the hope that it will be useful,
31
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
32
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33
 * GNU Affero General Public License for more details.
34
 *
35
 * You should have received a copy of the GNU Affero General Public License, version 3,
36
 * along with this program. If not, see <http://www.gnu.org/licenses/>
37
 *
38
 */
39
40
namespace OC\Share20;
41
42
use OC\Cache\CappedMemoryCache;
43
use OC\Files\Mount\MoveableMount;
44
use OC\HintException;
45
use OC\Share20\Exception\ProviderException;
46
use OCA\Files_Sharing\ISharedStorage;
47
use OCP\EventDispatcher\IEventDispatcher;
48
use OCP\Files\File;
49
use OCP\Files\Folder;
50
use OCP\Files\IRootFolder;
51
use OCP\Files\Mount\IMountManager;
52
use OCP\Files\Node;
53
use OCP\IConfig;
54
use OCP\IGroupManager;
55
use OCP\IL10N;
56
use OCP\ILogger;
57
use OCP\IURLGenerator;
58
use OCP\IUser;
59
use OCP\IUserManager;
60
use OCP\L10N\IFactory;
61
use OCP\Mail\IMailer;
62
use OCP\Security\Events\ValidatePasswordPolicyEvent;
63
use OCP\Security\IHasher;
64
use OCP\Security\ISecureRandom;
65
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...
66
use OCP\Share\Exceptions\AlreadySharedException;
67
use OCP\Share\Exceptions\GenericShareException;
68
use OCP\Share\Exceptions\ShareNotFound;
69
use OCP\Share\IManager;
70
use OCP\Share\IProviderFactory;
71
use OCP\Share\IShare;
72
use OCP\Share\IShareProvider;
73
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
74
use Symfony\Component\EventDispatcher\GenericEvent;
75
76
/**
77
 * This class is the communication hub for all sharing related operations.
78
 */
79
class Manager implements IManager {
80
81
	/** @var IProviderFactory */
82
	private $factory;
83
	/** @var ILogger */
84
	private $logger;
85
	/** @var IConfig */
86
	private $config;
87
	/** @var ISecureRandom */
88
	private $secureRandom;
89
	/** @var IHasher */
90
	private $hasher;
91
	/** @var IMountManager */
92
	private $mountManager;
93
	/** @var IGroupManager */
94
	private $groupManager;
95
	/** @var IL10N */
96
	private $l;
97
	/** @var IFactory */
98
	private $l10nFactory;
99
	/** @var IUserManager */
100
	private $userManager;
101
	/** @var IRootFolder */
102
	private $rootFolder;
103
	/** @var CappedMemoryCache */
104
	private $sharingDisabledForUsersCache;
105
	/** @var EventDispatcherInterface */
106
	private $legacyDispatcher;
107
	/** @var LegacyHooks */
108
	private $legacyHooks;
109
	/** @var IMailer */
110
	private $mailer;
111
	/** @var IURLGenerator */
112
	private $urlGenerator;
113
	/** @var \OC_Defaults */
114
	private $defaults;
115
	/** @var IEventDispatcher */
116
	private $dispatcher;
117
118
119
	/**
120
	 * Manager constructor.
121
	 *
122
	 * @param ILogger $logger
123
	 * @param IConfig $config
124
	 * @param ISecureRandom $secureRandom
125
	 * @param IHasher $hasher
126
	 * @param IMountManager $mountManager
127
	 * @param IGroupManager $groupManager
128
	 * @param IL10N $l
129
	 * @param IFactory $l10nFactory
130
	 * @param IProviderFactory $factory
131
	 * @param IUserManager $userManager
132
	 * @param IRootFolder $rootFolder
133
	 * @param EventDispatcherInterface $eventDispatcher
134
	 * @param IMailer $mailer
135
	 * @param IURLGenerator $urlGenerator
136
	 * @param \OC_Defaults $defaults
137
	 */
138
	public function __construct(
139
			ILogger $logger,
140
			IConfig $config,
141
			ISecureRandom $secureRandom,
142
			IHasher $hasher,
143
			IMountManager $mountManager,
144
			IGroupManager $groupManager,
145
			IL10N $l,
146
			IFactory $l10nFactory,
147
			IProviderFactory $factory,
148
			IUserManager $userManager,
149
			IRootFolder $rootFolder,
150
			EventDispatcherInterface $legacyDispatcher,
151
			IMailer $mailer,
152
			IURLGenerator $urlGenerator,
153
			\OC_Defaults $defaults,
154
			IEventDispatcher $dispatcher
155
	) {
156
		$this->logger = $logger;
157
		$this->config = $config;
158
		$this->secureRandom = $secureRandom;
159
		$this->hasher = $hasher;
160
		$this->mountManager = $mountManager;
161
		$this->groupManager = $groupManager;
162
		$this->l = $l;
163
		$this->l10nFactory = $l10nFactory;
164
		$this->factory = $factory;
165
		$this->userManager = $userManager;
166
		$this->rootFolder = $rootFolder;
167
		$this->legacyDispatcher = $legacyDispatcher;
168
		$this->sharingDisabledForUsersCache = new CappedMemoryCache();
169
		$this->legacyHooks = new LegacyHooks($this->legacyDispatcher);
170
		$this->mailer = $mailer;
171
		$this->urlGenerator = $urlGenerator;
172
		$this->defaults = $defaults;
173
		$this->dispatcher = $dispatcher;
174
	}
175
176
	/**
177
	 * Convert from a full share id to a tuple (providerId, shareId)
178
	 *
179
	 * @param string $id
180
	 * @return string[]
181
	 */
182
	private function splitFullId($id) {
183
		return explode(':', $id, 2);
184
	}
185
186
	/**
187
	 * Verify if a password meets all requirements
188
	 *
189
	 * @param string $password
190
	 * @throws \Exception
191
	 */
192
	protected function verifyPassword($password) {
193
		if ($password === null) {
0 ignored issues
show
introduced by
The condition $password === null is always false.
Loading history...
194
			// No password is set, check if this is allowed.
195
			if ($this->shareApiLinkEnforcePassword()) {
196
				throw new \InvalidArgumentException('Passwords are enforced for link and mail shares');
197
			}
198
199
			return;
200
		}
201
202
		// Let others verify the password
203
		try {
204
			$this->legacyDispatcher->dispatch(new ValidatePasswordPolicyEvent($password));
205
		} catch (HintException $e) {
206
			throw new \Exception($e->getHint());
207
		}
208
	}
209
210
	/**
211
	 * Check for generic requirements before creating a share
212
	 *
213
	 * @param IShare $share
214
	 * @throws \InvalidArgumentException
215
	 * @throws GenericShareException
216
	 *
217
	 * @suppress PhanUndeclaredClassMethod
218
	 */
219
	protected function generalCreateChecks(IShare $share) {
220
		if ($share->getShareType() === IShare::TYPE_USER) {
221
			// We expect a valid user as sharedWith for user shares
222
			if (!$this->userManager->userExists($share->getSharedWith())) {
223
				throw new \InvalidArgumentException('SharedWith is not a valid user');
224
			}
225
		} elseif ($share->getShareType() === IShare::TYPE_GROUP) {
226
			// We expect a valid group as sharedWith for group shares
227
			if (!$this->groupManager->groupExists($share->getSharedWith())) {
228
				throw new \InvalidArgumentException('SharedWith is not a valid group');
229
			}
230
		} elseif ($share->getShareType() === IShare::TYPE_LINK) {
231
			// No check for TYPE_EMAIL here as we have a recipient for them
232
			if ($share->getSharedWith() !== null) {
0 ignored issues
show
introduced by
The condition $share->getSharedWith() !== null is always true.
Loading history...
233
				throw new \InvalidArgumentException('SharedWith should be empty');
234
			}
235
		} elseif ($share->getShareType() === IShare::TYPE_EMAIL) {
236
			if ($share->getSharedWith() === null) {
0 ignored issues
show
introduced by
The condition $share->getSharedWith() === null is always false.
Loading history...
237
				throw new \InvalidArgumentException('SharedWith should not be empty');
238
			}
239
		} elseif ($share->getShareType() === IShare::TYPE_REMOTE) {
240
			if ($share->getSharedWith() === null) {
0 ignored issues
show
introduced by
The condition $share->getSharedWith() === null is always false.
Loading history...
241
				throw new \InvalidArgumentException('SharedWith should not be empty');
242
			}
243
		} elseif ($share->getShareType() === IShare::TYPE_REMOTE_GROUP) {
244
			if ($share->getSharedWith() === null) {
0 ignored issues
show
introduced by
The condition $share->getSharedWith() === null is always false.
Loading history...
245
				throw new \InvalidArgumentException('SharedWith should not be empty');
246
			}
247
		} elseif ($share->getShareType() === IShare::TYPE_CIRCLE) {
248
			$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...
249
			if ($circle === null) {
250
				throw new \InvalidArgumentException('SharedWith is not a valid circle');
251
			}
252
		} elseif ($share->getShareType() === IShare::TYPE_ROOM) {
253
		} elseif ($share->getShareType() === IShare::TYPE_DECK) {
254
		} else {
255
			// We can't handle other types yet
256
			throw new \InvalidArgumentException('unknown share type');
257
		}
258
259
		// Verify the initiator of the share is set
260
		if ($share->getSharedBy() === null) {
0 ignored issues
show
introduced by
The condition $share->getSharedBy() === null is always false.
Loading history...
261
			throw new \InvalidArgumentException('SharedBy should be set');
262
		}
263
264
		// Cannot share with yourself
265
		if ($share->getShareType() === IShare::TYPE_USER &&
266
			$share->getSharedWith() === $share->getSharedBy()) {
267
			throw new \InvalidArgumentException('Can’t share with yourself');
268
		}
269
270
		// The path should be set
271
		if ($share->getNode() === null) {
272
			throw new \InvalidArgumentException('Path should be set');
273
		}
274
275
		// And it should be a file or a folder
276
		if (!($share->getNode() instanceof \OCP\Files\File) &&
277
				!($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...
278
			throw new \InvalidArgumentException('Path should be either a file or a folder');
279
		}
280
281
		// And you can't share your rootfolder
282
		if ($this->userManager->userExists($share->getSharedBy())) {
283
			$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
284
		} else {
285
			$userFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
286
		}
287
		if ($userFolder->getId() === $share->getNode()->getId()) {
288
			throw new \InvalidArgumentException('You can’t share your root folder');
289
		}
290
291
		// Check if we actually have share permissions
292
		if (!$share->getNode()->isShareable()) {
293
			$message_t = $this->l->t('You are not allowed to share %s', [$share->getNode()->getName()]);
294
			throw new GenericShareException($message_t, $message_t, 404);
295
		}
296
297
		// Permissions should be set
298
		if ($share->getPermissions() === null) {
0 ignored issues
show
introduced by
The condition $share->getPermissions() === null is always false.
Loading history...
299
			throw new \InvalidArgumentException('A share requires permissions');
300
		}
301
302
		$isFederatedShare = $share->getNode()->getStorage()->instanceOfStorage('\OCA\Files_Sharing\External\Storage');
303
		$permissions = 0;
304
305
		if (!$isFederatedShare && $share->getNode()->getOwner() && $share->getNode()->getOwner()->getUID() !== $share->getSharedBy()) {
306
			$userMounts = array_filter($userFolder->getById($share->getNode()->getId()), function ($mount) {
307
				// We need to filter since there might be other mountpoints that contain the file
308
				// e.g. if the user has access to the same external storage that the file is originating from
309
				return $mount->getStorage()->instanceOfStorage(ISharedStorage::class);
310
			});
311
			$userMount = array_shift($userMounts);
312
			if ($userMount === null) {
313
				throw new GenericShareException('Could not get proper share mount for ' . $share->getNode()->getId() . '. Failing since else the next calls are called with null');
314
			}
315
			$mount = $userMount->getMountPoint();
316
			// When it's a reshare use the parent share permissions as maximum
317
			$userMountPointId = $mount->getStorageRootId();
318
			$userMountPoints = $userFolder->getById($userMountPointId);
319
			$userMountPoint = array_shift($userMountPoints);
320
321
			if ($userMountPoint === null) {
322
				throw new GenericShareException('Could not get proper user mount for ' . $userMountPointId . '. Failing since else the next calls are called with null');
323
			}
324
325
			/* Check if this is an incoming share */
326
			$incomingShares = $this->getSharedWith($share->getSharedBy(), IShare::TYPE_USER, $userMountPoint, -1, 0);
327
			$incomingShares = array_merge($incomingShares, $this->getSharedWith($share->getSharedBy(), IShare::TYPE_GROUP, $userMountPoint, -1, 0));
328
			$incomingShares = array_merge($incomingShares, $this->getSharedWith($share->getSharedBy(), IShare::TYPE_CIRCLE, $userMountPoint, -1, 0));
329
			$incomingShares = array_merge($incomingShares, $this->getSharedWith($share->getSharedBy(), IShare::TYPE_ROOM, $userMountPoint, -1, 0));
330
331
			/** @var IShare[] $incomingShares */
332
			if (!empty($incomingShares)) {
333
				foreach ($incomingShares as $incomingShare) {
334
					$permissions |= $incomingShare->getPermissions();
335
				}
336
			}
337
		} else {
338
			/*
339
			 * Quick fix for #23536
340
			 * Non moveable mount points do not have update and delete permissions
341
			 * while we 'most likely' do have that on the storage.
342
			 */
343
			$permissions = $share->getNode()->getPermissions();
344
			if (!($share->getNode()->getMountPoint() instanceof MoveableMount)) {
345
				$permissions |= \OCP\Constants::PERMISSION_DELETE | \OCP\Constants::PERMISSION_UPDATE;
346
			}
347
		}
348
349
		// Check that we do not share with more permissions than we have
350
		if ($share->getPermissions() & ~$permissions) {
351
			$path = $userFolder->getRelativePath($share->getNode()->getPath());
352
			$message_t = $this->l->t('Can’t increase permissions of %s', [$path]);
353
			throw new GenericShareException($message_t, $message_t, 404);
354
		}
355
356
357
		// Check that read permissions are always set
358
		// Link shares are allowed to have no read permissions to allow upload to hidden folders
359
		$noReadPermissionRequired = $share->getShareType() === IShare::TYPE_LINK
360
			|| $share->getShareType() === IShare::TYPE_EMAIL;
361
		if (!$noReadPermissionRequired &&
362
			($share->getPermissions() & \OCP\Constants::PERMISSION_READ) === 0) {
363
			throw new \InvalidArgumentException('Shares need at least read permissions');
364
		}
365
366
		if ($share->getNode() instanceof \OCP\Files\File) {
367
			if ($share->getPermissions() & \OCP\Constants::PERMISSION_DELETE) {
368
				$message_t = $this->l->t('Files can’t be shared with delete permissions');
369
				throw new GenericShareException($message_t);
370
			}
371
			if ($share->getPermissions() & \OCP\Constants::PERMISSION_CREATE) {
372
				$message_t = $this->l->t('Files can’t be shared with create permissions');
373
				throw new GenericShareException($message_t);
374
			}
375
		}
376
	}
377
378
	/**
379
	 * Validate if the expiration date fits the system settings
380
	 *
381
	 * @param IShare $share The share to validate the expiration date of
382
	 * @return IShare The modified share object
383
	 * @throws GenericShareException
384
	 * @throws \InvalidArgumentException
385
	 * @throws \Exception
386
	 */
387
	protected function validateExpirationDateInternal(IShare $share) {
388
		$isRemote = $share->getShareType() === IShare::TYPE_REMOTE || $share->getShareType() === IShare::TYPE_REMOTE_GROUP;
389
390
		$expirationDate = $share->getExpirationDate();
391
392
		if ($expirationDate !== null) {
393
			//Make sure the expiration date is a date
394
			$expirationDate->setTime(0, 0, 0);
395
396
			$date = new \DateTime();
397
			$date->setTime(0, 0, 0);
398
			if ($date >= $expirationDate) {
399
				$message = $this->l->t('Expiration date is in the past');
400
				throw new GenericShareException($message, $message, 404);
401
			}
402
		}
403
404
		// If expiredate is empty set a default one if there is a default
405
		$fullId = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $fullId is dead and can be removed.
Loading history...
406
		try {
407
			$fullId = $share->getFullId();
408
		} catch (\UnexpectedValueException $e) {
409
			// This is a new share
410
		}
411
412
		if ($isRemote) {
413
			$defaultExpireDate = $this->shareApiRemoteDefaultExpireDate();
414
			$defaultExpireDays = $this->shareApiRemoteDefaultExpireDays();
415
			$configProp = 'remote_defaultExpDays';
416
			$isEnforced = $this->shareApiRemoteDefaultExpireDateEnforced();
417
		} else {
418
			$defaultExpireDate = $this->shareApiInternalDefaultExpireDate();
419
			$defaultExpireDays = $this->shareApiInternalDefaultExpireDays();
420
			$configProp = 'internal_defaultExpDays';
421
			$isEnforced = $this->shareApiInternalDefaultExpireDateEnforced();
422
		}
423
		if ($fullId === null && $expirationDate === null && $defaultExpireDate) {
0 ignored issues
show
introduced by
The condition $fullId === null is always false.
Loading history...
424
			$expirationDate = new \DateTime();
425
			$expirationDate->setTime(0,0,0);
426
427
			$days = (int)$this->config->getAppValue('core', $configProp, (string)$defaultExpireDays);
428
			if ($days > $defaultExpireDays) {
429
				$days = $defaultExpireDays;
430
			}
431
			$expirationDate->add(new \DateInterval('P'.$days.'D'));
432
		}
433
434
		// If we enforce the expiration date check that is does not exceed
435
		if ($isEnforced) {
436
			if ($expirationDate === null) {
437
				throw new \InvalidArgumentException('Expiration date is enforced');
438
			}
439
440
			$date = new \DateTime();
441
			$date->setTime(0, 0, 0);
442
			$date->add(new \DateInterval('P' . $defaultExpireDays . 'D'));
443
			if ($date < $expirationDate) {
444
				$message = $this->l->t('Can’t set expiration date more than %s days in the future', [$defaultExpireDays]);
445
				throw new GenericShareException($message, $message, 404);
446
			}
447
		}
448
449
		$accepted = true;
450
		$message = '';
451
		\OCP\Util::emitHook('\OC\Share', 'verifyExpirationDate', [
452
			'expirationDate' => &$expirationDate,
453
			'accepted' => &$accepted,
454
			'message' => &$message,
455
			'passwordSet' => $share->getPassword() !== null,
456
		]);
457
458
		if (!$accepted) {
0 ignored issues
show
introduced by
The condition $accepted is always true.
Loading history...
459
			throw new \Exception($message);
460
		}
461
462
		$share->setExpirationDate($expirationDate);
463
464
		return $share;
465
	}
466
467
	/**
468
	 * Validate if the expiration date fits the system settings
469
	 *
470
	 * @param IShare $share The share to validate the expiration date of
471
	 * @return IShare The modified share object
472
	 * @throws GenericShareException
473
	 * @throws \InvalidArgumentException
474
	 * @throws \Exception
475
	 */
476
	protected function validateExpirationDateLink(IShare $share) {
477
		$expirationDate = $share->getExpirationDate();
478
479
		if ($expirationDate !== null) {
480
			//Make sure the expiration date is a date
481
			$expirationDate->setTime(0, 0, 0);
482
483
			$date = new \DateTime();
484
			$date->setTime(0, 0, 0);
485
			if ($date >= $expirationDate) {
486
				$message = $this->l->t('Expiration date is in the past');
487
				throw new GenericShareException($message, $message, 404);
488
			}
489
		}
490
491
		// If expiredate is empty set a default one if there is a default
492
		$fullId = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $fullId is dead and can be removed.
Loading history...
493
		try {
494
			$fullId = $share->getFullId();
495
		} catch (\UnexpectedValueException $e) {
496
			// This is a new share
497
		}
498
499
		if ($fullId === null && $expirationDate === null && $this->shareApiLinkDefaultExpireDate()) {
0 ignored issues
show
introduced by
The condition $fullId === null is always false.
Loading history...
500
			$expirationDate = new \DateTime();
501
			$expirationDate->setTime(0,0,0);
502
503
			$days = (int)$this->config->getAppValue('core', 'link_defaultExpDays', $this->shareApiLinkDefaultExpireDays());
504
			if ($days > $this->shareApiLinkDefaultExpireDays()) {
505
				$days = $this->shareApiLinkDefaultExpireDays();
506
			}
507
			$expirationDate->add(new \DateInterval('P'.$days.'D'));
508
		}
509
510
		// If we enforce the expiration date check that is does not exceed
511
		if ($this->shareApiLinkDefaultExpireDateEnforced()) {
512
			if ($expirationDate === null) {
513
				throw new \InvalidArgumentException('Expiration date is enforced');
514
			}
515
516
			$date = new \DateTime();
517
			$date->setTime(0, 0, 0);
518
			$date->add(new \DateInterval('P' . $this->shareApiLinkDefaultExpireDays() . 'D'));
519
			if ($date < $expirationDate) {
520
				$message = $this->l->t('Can’t set expiration date more than %s days in the future', [$this->shareApiLinkDefaultExpireDays()]);
521
				throw new GenericShareException($message, $message, 404);
522
			}
523
		}
524
525
		$accepted = true;
526
		$message = '';
527
		\OCP\Util::emitHook('\OC\Share', 'verifyExpirationDate', [
528
			'expirationDate' => &$expirationDate,
529
			'accepted' => &$accepted,
530
			'message' => &$message,
531
			'passwordSet' => $share->getPassword() !== null,
532
		]);
533
534
		if (!$accepted) {
0 ignored issues
show
introduced by
The condition $accepted is always true.
Loading history...
535
			throw new \Exception($message);
536
		}
537
538
		$share->setExpirationDate($expirationDate);
539
540
		return $share;
541
	}
542
543
	/**
544
	 * Check for pre share requirements for user shares
545
	 *
546
	 * @param IShare $share
547
	 * @throws \Exception
548
	 */
549
	protected function userCreateChecks(IShare $share) {
550
		// Check if we can share with group members only
551
		if ($this->shareWithGroupMembersOnly()) {
552
			$sharedBy = $this->userManager->get($share->getSharedBy());
553
			$sharedWith = $this->userManager->get($share->getSharedWith());
554
			// Verify we can share with this user
555
			$groups = array_intersect(
556
					$this->groupManager->getUserGroupIds($sharedBy),
557
					$this->groupManager->getUserGroupIds($sharedWith)
558
			);
559
			if (empty($groups)) {
560
				$message_t = $this->l->t('Sharing is only allowed with group members');
561
				throw new \Exception($message_t);
562
			}
563
		}
564
565
		/*
566
		 * TODO: Could be costly, fix
567
		 *
568
		 * Also this is not what we want in the future.. then we want to squash identical shares.
569
		 */
570
		$provider = $this->factory->getProviderForType(IShare::TYPE_USER);
571
		$existingShares = $provider->getSharesByPath($share->getNode());
572
		foreach ($existingShares as $existingShare) {
573
			// Ignore if it is the same share
574
			try {
575
				if ($existingShare->getFullId() === $share->getFullId()) {
576
					continue;
577
				}
578
			} catch (\UnexpectedValueException $e) {
579
				//Shares are not identical
580
			}
581
582
			// Identical share already exists
583
			if ($existingShare->getSharedWith() === $share->getSharedWith() && $existingShare->getShareType() === $share->getShareType()) {
584
				$message = $this->l->t('Sharing %s failed, because this item is already shared with user %s', [$share->getNode()->getName(), $share->getSharedWithDisplayName()]);
585
				throw new AlreadySharedException($message, $existingShare);
586
			}
587
588
			// The share is already shared with this user via a group share
589
			if ($existingShare->getShareType() === IShare::TYPE_GROUP) {
590
				$group = $this->groupManager->get($existingShare->getSharedWith());
591
				if (!is_null($group)) {
592
					$user = $this->userManager->get($share->getSharedWith());
593
594
					if ($group->inGroup($user) && $existingShare->getShareOwner() !== $share->getShareOwner()) {
595
						$message = $this->l->t('Sharing %s failed, because this item is already shared with user %s', [$share->getNode()->getName(), $share->getSharedWithDisplayName()]);
596
						throw new AlreadySharedException($message, $existingShare);
597
					}
598
				}
599
			}
600
		}
601
	}
602
603
	/**
604
	 * Check for pre share requirements for group shares
605
	 *
606
	 * @param IShare $share
607
	 * @throws \Exception
608
	 */
609
	protected function groupCreateChecks(IShare $share) {
610
		// Verify group shares are allowed
611
		if (!$this->allowGroupSharing()) {
612
			throw new \Exception('Group sharing is now allowed');
613
		}
614
615
		// Verify if the user can share with this group
616
		if ($this->shareWithGroupMembersOnly()) {
617
			$sharedBy = $this->userManager->get($share->getSharedBy());
618
			$sharedWith = $this->groupManager->get($share->getSharedWith());
619
			if (is_null($sharedWith) || !$sharedWith->inGroup($sharedBy)) {
620
				throw new \Exception('Sharing is only allowed within your own groups');
621
			}
622
		}
623
624
		/*
625
		 * TODO: Could be costly, fix
626
		 *
627
		 * Also this is not what we want in the future.. then we want to squash identical shares.
628
		 */
629
		$provider = $this->factory->getProviderForType(IShare::TYPE_GROUP);
630
		$existingShares = $provider->getSharesByPath($share->getNode());
631
		foreach ($existingShares as $existingShare) {
632
			try {
633
				if ($existingShare->getFullId() === $share->getFullId()) {
634
					continue;
635
				}
636
			} catch (\UnexpectedValueException $e) {
637
				//It is a new share so just continue
638
			}
639
640
			if ($existingShare->getSharedWith() === $share->getSharedWith() && $existingShare->getShareType() === $share->getShareType()) {
641
				throw new AlreadySharedException('Path is already shared with this group', $existingShare);
642
			}
643
		}
644
	}
645
646
	/**
647
	 * Check for pre share requirements for link shares
648
	 *
649
	 * @param IShare $share
650
	 * @throws \Exception
651
	 */
652
	protected function linkCreateChecks(IShare $share) {
653
		// Are link shares allowed?
654
		if (!$this->shareApiAllowLinks()) {
655
			throw new \Exception('Link sharing is not allowed');
656
		}
657
658
		// Check if public upload is allowed
659
		if (!$this->shareApiLinkAllowPublicUpload() &&
660
			($share->getPermissions() & (\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE))) {
661
			throw new \InvalidArgumentException('Public upload is not allowed');
662
		}
663
	}
664
665
	/**
666
	 * To make sure we don't get invisible link shares we set the parent
667
	 * of a link if it is a reshare. This is a quick word around
668
	 * until we can properly display multiple link shares in the UI
669
	 *
670
	 * See: https://github.com/owncloud/core/issues/22295
671
	 *
672
	 * FIXME: Remove once multiple link shares can be properly displayed
673
	 *
674
	 * @param IShare $share
675
	 */
676
	protected function setLinkParent(IShare $share) {
677
678
		// No sense in checking if the method is not there.
679
		if (method_exists($share, 'setParent')) {
680
			$storage = $share->getNode()->getStorage();
681
			if ($storage->instanceOfStorage('\OCA\Files_Sharing\ISharedStorage')) {
682
				/** @var \OCA\Files_Sharing\SharedStorage $storage */
683
				$share->setParent($storage->getShareId());
684
			}
685
		}
686
	}
687
688
	/**
689
	 * @param File|Folder $path
690
	 */
691
	protected function pathCreateChecks($path) {
692
		// Make sure that we do not share a path that contains a shared mountpoint
693
		if ($path instanceof \OCP\Files\Folder) {
694
			$mounts = $this->mountManager->findIn($path->getPath());
695
			foreach ($mounts as $mount) {
696
				if ($mount->getStorage()->instanceOfStorage('\OCA\Files_Sharing\ISharedStorage')) {
697
					throw new \InvalidArgumentException('Path contains files shared with you');
698
				}
699
			}
700
		}
701
	}
702
703
	/**
704
	 * Check if the user that is sharing can actually share
705
	 *
706
	 * @param IShare $share
707
	 * @throws \Exception
708
	 */
709
	protected function canShare(IShare $share) {
710
		if (!$this->shareApiEnabled()) {
711
			throw new \Exception('Sharing is disabled');
712
		}
713
714
		if ($this->sharingDisabledForUser($share->getSharedBy())) {
715
			throw new \Exception('Sharing is disabled for you');
716
		}
717
	}
718
719
	/**
720
	 * Share a path
721
	 *
722
	 * @param IShare $share
723
	 * @return IShare The share object
724
	 * @throws \Exception
725
	 *
726
	 * TODO: handle link share permissions or check them
727
	 */
728
	public function createShare(IShare $share) {
729
		$this->canShare($share);
730
731
		$this->generalCreateChecks($share);
732
733
		// Verify if there are any issues with the path
734
		$this->pathCreateChecks($share->getNode());
735
736
		/*
737
		 * On creation of a share the owner is always the owner of the path
738
		 * Except for mounted federated shares.
739
		 */
740
		$storage = $share->getNode()->getStorage();
741
		if ($storage->instanceOfStorage('OCA\Files_Sharing\External\Storage')) {
742
			$parent = $share->getNode()->getParent();
743
			while ($parent->getStorage()->instanceOfStorage('OCA\Files_Sharing\External\Storage')) {
744
				$parent = $parent->getParent();
745
			}
746
			$share->setShareOwner($parent->getOwner()->getUID());
747
		} else {
748
			if ($share->getNode()->getOwner()) {
749
				$share->setShareOwner($share->getNode()->getOwner()->getUID());
750
			} else {
751
				$share->setShareOwner($share->getSharedBy());
752
			}
753
		}
754
755
		try {
756
			// Verify share type
757
			if ($share->getShareType() === IShare::TYPE_USER) {
758
				$this->userCreateChecks($share);
759
760
				// Verify the expiration date
761
				$share = $this->validateExpirationDateInternal($share);
762
			} elseif ($share->getShareType() === IShare::TYPE_GROUP) {
763
				$this->groupCreateChecks($share);
764
765
				// Verify the expiration date
766
				$share = $this->validateExpirationDateInternal($share);
767
			} elseif ($share->getShareType() === IShare::TYPE_REMOTE || $share->getShareType() === IShare::TYPE_REMOTE_GROUP) {
768
				//Verify the expiration date
769
				$share = $this->validateExpirationDateInternal($share);
770
			} elseif ($share->getShareType() === IShare::TYPE_LINK
771
				|| $share->getShareType() === IShare::TYPE_EMAIL) {
772
				$this->linkCreateChecks($share);
773
				$this->setLinkParent($share);
774
775
				// For now ignore a set token.
776
				$share->setToken(
777
					$this->secureRandom->generate(
778
						\OC\Share\Constants::TOKEN_LENGTH,
779
						\OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE
780
					)
781
				);
782
783
				// Verify the expiration date
784
				$share = $this->validateExpirationDateLink($share);
785
786
				// Verify the password
787
				$this->verifyPassword($share->getPassword());
788
789
				// If a password is set. Hash it!
790
				if ($share->getShareType() === IShare::TYPE_LINK
791
					&& $share->getPassword() !== null) {
792
					$share->setPassword($this->hasher->hash($share->getPassword()));
793
				}
794
			}
795
796
			// Cannot share with the owner
797
			if ($share->getShareType() === IShare::TYPE_USER &&
798
				$share->getSharedWith() === $share->getShareOwner()) {
799
				throw new \InvalidArgumentException('Can’t share with the share owner');
800
			}
801
802
			// Generate the target
803
			$target = $this->config->getSystemValue('share_folder', '/') . '/' . $share->getNode()->getName();
804
			$target = \OC\Files\Filesystem::normalizePath($target);
805
			$share->setTarget($target);
806
807
			// Pre share event
808
			$event = new GenericEvent($share);
809
			$this->legacyDispatcher->dispatch('OCP\Share::preShare', $event);
0 ignored issues
show
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

809
			$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ 'OCP\Share::preShare', $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

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

1051
			/** @scrutinizer ignore-call */ 
1052
   $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...
1052
		} else {
1053
			$share = $provider->update($share);
1054
		}
1055
1056
		if ($expirationDateUpdated === true) {
1057
			\OC_Hook::emit(Share::class, 'post_set_expiration_date', [
1058
				'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
1059
				'itemSource' => $share->getNode()->getId(),
1060
				'date' => $share->getExpirationDate(),
1061
				'uidOwner' => $share->getSharedBy(),
1062
			]);
1063
		}
1064
1065
		if ($share->getPassword() !== $originalShare->getPassword()) {
1066
			\OC_Hook::emit(Share::class, 'post_update_password', [
1067
				'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
1068
				'itemSource' => $share->getNode()->getId(),
1069
				'uidOwner' => $share->getSharedBy(),
1070
				'token' => $share->getToken(),
1071
				'disabled' => is_null($share->getPassword()),
1072
			]);
1073
		}
1074
1075
		if ($share->getPermissions() !== $originalShare->getPermissions()) {
1076
			if ($this->userManager->userExists($share->getShareOwner())) {
1077
				$userFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
1078
			} else {
1079
				$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
1080
			}
1081
			\OC_Hook::emit(Share::class, 'post_update_permissions', [
1082
				'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
1083
				'itemSource' => $share->getNode()->getId(),
1084
				'shareType' => $share->getShareType(),
1085
				'shareWith' => $share->getSharedWith(),
1086
				'uidOwner' => $share->getSharedBy(),
1087
				'permissions' => $share->getPermissions(),
1088
				'path' => $userFolder->getRelativePath($share->getNode()->getPath()),
1089
			]);
1090
		}
1091
1092
		return $share;
1093
	}
1094
1095
	/**
1096
	 * Accept a share.
1097
	 *
1098
	 * @param IShare $share
1099
	 * @param string $recipientId
1100
	 * @return IShare The share object
1101
	 * @throws \InvalidArgumentException
1102
	 * @since 9.0.0
1103
	 */
1104
	public function acceptShare(IShare $share, string $recipientId): IShare {
1105
		[$providerId, ] = $this->splitFullId($share->getFullId());
1106
		$provider = $this->factory->getProvider($providerId);
1107
1108
		if (!method_exists($provider, 'acceptShare')) {
1109
			// TODO FIX ME
1110
			throw new \InvalidArgumentException('Share provider does not support accepting');
1111
		}
1112
		$provider->acceptShare($share, $recipientId);
1113
		$event = new GenericEvent($share);
1114
		$this->legacyDispatcher->dispatch('OCP\Share::postAcceptShare', $event);
0 ignored issues
show
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

1114
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ 'OCP\Share::postAcceptShare', $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

1114
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
1115
                           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...
1115
1116
		return $share;
1117
	}
1118
1119
	/**
1120
	 * Updates the password of the given share if it is not the same as the
1121
	 * password of the original share.
1122
	 *
1123
	 * @param IShare $share the share to update its password.
1124
	 * @param IShare $originalShare the original share to compare its
1125
	 *        password with.
1126
	 * @return boolean whether the password was updated or not.
1127
	 */
1128
	private function updateSharePasswordIfNeeded(IShare $share, IShare $originalShare) {
1129
		$passwordsAreDifferent = ($share->getPassword() !== $originalShare->getPassword()) &&
1130
									(($share->getPassword() !== null && $originalShare->getPassword() === null) ||
0 ignored issues
show
introduced by
The condition $originalShare->getPassword() === null is always false.
Loading history...
1131
									 ($share->getPassword() === null && $originalShare->getPassword() !== null) ||
0 ignored issues
show
introduced by
The condition $share->getPassword() === null is always false.
Loading history...
1132
									 ($share->getPassword() !== null && $originalShare->getPassword() !== null &&
1133
										!$this->hasher->verify($share->getPassword(), $originalShare->getPassword())));
1134
1135
		// Password updated.
1136
		if ($passwordsAreDifferent) {
1137
			//Verify the password
1138
			$this->verifyPassword($share->getPassword());
1139
1140
			// If a password is set. Hash it!
1141
			if (!empty($share->getPassword())) {
1142
				$share->setPassword($this->hasher->hash($share->getPassword()));
1143
1144
				return true;
1145
			} else {
1146
				// Empty string and null are seen as NOT password protected
1147
				$share->setPassword(null);
1148
				return true;
1149
			}
1150
		} else {
1151
			// Reset the password to the original one, as it is either the same
1152
			// as the "new" password or a hashed version of it.
1153
			$share->setPassword($originalShare->getPassword());
1154
		}
1155
1156
		return false;
1157
	}
1158
1159
	/**
1160
	 * Delete all the children of this share
1161
	 * FIXME: remove once https://github.com/owncloud/core/pull/21660 is in
1162
	 *
1163
	 * @param IShare $share
1164
	 * @return IShare[] List of deleted shares
1165
	 */
1166
	protected function deleteChildren(IShare $share) {
1167
		$deletedShares = [];
1168
1169
		$provider = $this->factory->getProviderForType($share->getShareType());
1170
1171
		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

1171
		foreach ($provider->/** @scrutinizer ignore-call */ getChildren($share) as $child) {
Loading history...
1172
			$deletedChildren = $this->deleteChildren($child);
1173
			$deletedShares = array_merge($deletedShares, $deletedChildren);
1174
1175
			$provider->delete($child);
1176
			$this->dispatcher->dispatchTyped(new Share\Events\ShareDeletedEvent($child));
1177
			$deletedShares[] = $child;
1178
		}
1179
1180
		return $deletedShares;
1181
	}
1182
1183
	/**
1184
	 * Delete a share
1185
	 *
1186
	 * @param IShare $share
1187
	 * @throws ShareNotFound
1188
	 * @throws \InvalidArgumentException
1189
	 */
1190
	public function deleteShare(IShare $share) {
1191
		try {
1192
			$share->getFullId();
1193
		} catch (\UnexpectedValueException $e) {
1194
			throw new \InvalidArgumentException('Share does not have a full id');
1195
		}
1196
1197
		$event = new GenericEvent($share);
1198
		$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

1198
		$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

1198
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
1199
                           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...
1199
1200
		// Get all children and delete them as well
1201
		$deletedShares = $this->deleteChildren($share);
1202
1203
		// Do the actual delete
1204
		$provider = $this->factory->getProviderForType($share->getShareType());
1205
		$provider->delete($share);
1206
1207
		$this->dispatcher->dispatchTyped(new Share\Events\ShareDeletedEvent($share));
1208
1209
		// All the deleted shares caused by this delete
1210
		$deletedShares[] = $share;
1211
1212
		// Emit post hook
1213
		$event->setArgument('deletedShares', $deletedShares);
1214
		$this->legacyDispatcher->dispatch('OCP\Share::postUnshare', $event);
1215
	}
1216
1217
1218
	/**
1219
	 * Unshare a file as the recipient.
1220
	 * This can be different from a regular delete for example when one of
1221
	 * the users in a groups deletes that share. But the provider should
1222
	 * handle this.
1223
	 *
1224
	 * @param IShare $share
1225
	 * @param string $recipientId
1226
	 */
1227
	public function deleteFromSelf(IShare $share, $recipientId) {
1228
		[$providerId, ] = $this->splitFullId($share->getFullId());
1229
		$provider = $this->factory->getProvider($providerId);
1230
1231
		$provider->deleteFromSelf($share, $recipientId);
1232
		$event = new GenericEvent($share);
1233
		$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

1233
		$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

1233
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
1234
                           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...
1234
	}
1235
1236
	public function restoreShare(IShare $share, string $recipientId): IShare {
1237
		[$providerId, ] = $this->splitFullId($share->getFullId());
1238
		$provider = $this->factory->getProvider($providerId);
1239
1240
		return $provider->restore($share, $recipientId);
1241
	}
1242
1243
	/**
1244
	 * @inheritdoc
1245
	 */
1246
	public function moveShare(IShare $share, $recipientId) {
1247
		if ($share->getShareType() === IShare::TYPE_LINK
1248
			|| $share->getShareType() === IShare::TYPE_EMAIL) {
1249
			throw new \InvalidArgumentException('Can’t change target of link share');
1250
		}
1251
1252
		if ($share->getShareType() === IShare::TYPE_USER && $share->getSharedWith() !== $recipientId) {
1253
			throw new \InvalidArgumentException('Invalid recipient');
1254
		}
1255
1256
		if ($share->getShareType() === IShare::TYPE_GROUP) {
1257
			$sharedWith = $this->groupManager->get($share->getSharedWith());
1258
			if (is_null($sharedWith)) {
1259
				throw new \InvalidArgumentException('Group "' . $share->getSharedWith() . '" does not exist');
1260
			}
1261
			$recipient = $this->userManager->get($recipientId);
1262
			if (!$sharedWith->inGroup($recipient)) {
1263
				throw new \InvalidArgumentException('Invalid recipient');
1264
			}
1265
		}
1266
1267
		[$providerId, ] = $this->splitFullId($share->getFullId());
1268
		$provider = $this->factory->getProvider($providerId);
1269
1270
		return $provider->move($share, $recipientId);
1271
	}
1272
1273
	public function getSharesInFolder($userId, Folder $node, $reshares = false) {
1274
		$providers = $this->factory->getAllProviders();
1275
1276
		return array_reduce($providers, function ($shares, IShareProvider $provider) use ($userId, $node, $reshares) {
1277
			$newShares = $provider->getSharesInFolder($userId, $node, $reshares);
1278
			foreach ($newShares as $fid => $data) {
1279
				if (!isset($shares[$fid])) {
1280
					$shares[$fid] = [];
1281
				}
1282
1283
				$shares[$fid] = array_merge($shares[$fid], $data);
1284
			}
1285
			return $shares;
1286
		}, []);
1287
	}
1288
1289
	/**
1290
	 * @inheritdoc
1291
	 */
1292
	public function getSharesBy($userId, $shareType, $path = null, $reshares = false, $limit = 50, $offset = 0) {
1293
		if ($path !== null &&
1294
				!($path instanceof \OCP\Files\File) &&
1295
				!($path instanceof \OCP\Files\Folder)) {
1296
			throw new \InvalidArgumentException('invalid path');
1297
		}
1298
1299
		try {
1300
			$provider = $this->factory->getProviderForType($shareType);
1301
		} catch (ProviderException $e) {
1302
			return [];
1303
		}
1304
1305
		$shares = $provider->getSharesBy($userId, $shareType, $path, $reshares, $limit, $offset);
1306
1307
		/*
1308
		 * Work around so we don't return expired shares but still follow
1309
		 * proper pagination.
1310
		 */
1311
1312
		$shares2 = [];
1313
1314
		while (true) {
1315
			$added = 0;
1316
			foreach ($shares as $share) {
1317
				try {
1318
					$this->checkExpireDate($share);
1319
				} catch (ShareNotFound $e) {
1320
					//Ignore since this basically means the share is deleted
1321
					continue;
1322
				}
1323
1324
				$added++;
1325
				$shares2[] = $share;
1326
1327
				if (count($shares2) === $limit) {
1328
					break;
1329
				}
1330
			}
1331
1332
			// If we did not fetch more shares than the limit then there are no more shares
1333
			if (count($shares) < $limit) {
1334
				break;
1335
			}
1336
1337
			if (count($shares2) === $limit) {
1338
				break;
1339
			}
1340
1341
			// If there was no limit on the select we are done
1342
			if ($limit === -1) {
1343
				break;
1344
			}
1345
1346
			$offset += $added;
1347
1348
			// Fetch again $limit shares
1349
			$shares = $provider->getSharesBy($userId, $shareType, $path, $reshares, $limit, $offset);
1350
1351
			// No more shares means we are done
1352
			if (empty($shares)) {
1353
				break;
1354
			}
1355
		}
1356
1357
		$shares = $shares2;
1358
1359
		return $shares;
1360
	}
1361
1362
	/**
1363
	 * @inheritdoc
1364
	 */
1365
	public function getSharedWith($userId, $shareType, $node = null, $limit = 50, $offset = 0) {
1366
		try {
1367
			$provider = $this->factory->getProviderForType($shareType);
1368
		} catch (ProviderException $e) {
1369
			return [];
1370
		}
1371
1372
		$shares = $provider->getSharedWith($userId, $shareType, $node, $limit, $offset);
1373
1374
		// remove all shares which are already expired
1375
		foreach ($shares as $key => $share) {
1376
			try {
1377
				$this->checkExpireDate($share);
1378
			} catch (ShareNotFound $e) {
1379
				unset($shares[$key]);
1380
			}
1381
		}
1382
1383
		return $shares;
1384
	}
1385
1386
	/**
1387
	 * @inheritdoc
1388
	 */
1389
	public function getDeletedSharedWith($userId, $shareType, $node = null, $limit = 50, $offset = 0) {
1390
		$shares = $this->getSharedWith($userId, $shareType, $node, $limit, $offset);
1391
1392
		// Only get deleted shares
1393
		$shares = array_filter($shares, function (IShare $share) {
1394
			return $share->getPermissions() === 0;
1395
		});
1396
1397
		// Only get shares where the owner still exists
1398
		$shares = array_filter($shares, function (IShare $share) {
1399
			return $this->userManager->userExists($share->getShareOwner());
1400
		});
1401
1402
		return $shares;
1403
	}
1404
1405
	/**
1406
	 * @inheritdoc
1407
	 */
1408
	public function getShareById($id, $recipient = null) {
1409
		if ($id === null) {
1410
			throw new ShareNotFound();
1411
		}
1412
1413
		[$providerId, $id] = $this->splitFullId($id);
1414
1415
		try {
1416
			$provider = $this->factory->getProvider($providerId);
1417
		} catch (ProviderException $e) {
1418
			throw new ShareNotFound();
1419
		}
1420
1421
		$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

1421
		$share = $provider->getShareById(/** @scrutinizer ignore-type */ $id, $recipient);
Loading history...
1422
1423
		$this->checkExpireDate($share);
1424
1425
		return $share;
1426
	}
1427
1428
	/**
1429
	 * Get all the shares for a given path
1430
	 *
1431
	 * @param \OCP\Files\Node $path
1432
	 * @param int $page
1433
	 * @param int $perPage
1434
	 *
1435
	 * @return Share[]
1436
	 */
1437
	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

1437
	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 $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

1437
	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...
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

1437
	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...
1438
		return [];
1439
	}
1440
1441
	/**
1442
	 * Get the share by token possible with password
1443
	 *
1444
	 * @param string $token
1445
	 * @return IShare
1446
	 *
1447
	 * @throws ShareNotFound
1448
	 */
1449
	public function getShareByToken($token) {
1450
		// tokens can't be valid local user names
1451
		if ($this->userManager->userExists($token)) {
1452
			throw new ShareNotFound();
1453
		}
1454
		$share = null;
1455
		try {
1456
			if ($this->shareApiAllowLinks()) {
1457
				$provider = $this->factory->getProviderForType(IShare::TYPE_LINK);
1458
				$share = $provider->getShareByToken($token);
1459
			}
1460
		} catch (ProviderException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1461
		} catch (ShareNotFound $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1462
		}
1463
1464
1465
		// If it is not a link share try to fetch a federated share by token
1466
		if ($share === null) {
1467
			try {
1468
				$provider = $this->factory->getProviderForType(IShare::TYPE_REMOTE);
1469
				$share = $provider->getShareByToken($token);
1470
			} catch (ProviderException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1471
			} catch (ShareNotFound $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1472
			}
1473
		}
1474
1475
		// If it is not a link share try to fetch a mail share by token
1476
		if ($share === null && $this->shareProviderExists(IShare::TYPE_EMAIL)) {
1477
			try {
1478
				$provider = $this->factory->getProviderForType(IShare::TYPE_EMAIL);
1479
				$share = $provider->getShareByToken($token);
1480
			} catch (ProviderException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1481
			} catch (ShareNotFound $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1482
			}
1483
		}
1484
1485
		if ($share === null && $this->shareProviderExists(IShare::TYPE_CIRCLE)) {
1486
			try {
1487
				$provider = $this->factory->getProviderForType(IShare::TYPE_CIRCLE);
1488
				$share = $provider->getShareByToken($token);
1489
			} catch (ProviderException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1490
			} catch (ShareNotFound $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1491
			}
1492
		}
1493
1494
		if ($share === null && $this->shareProviderExists(IShare::TYPE_ROOM)) {
1495
			try {
1496
				$provider = $this->factory->getProviderForType(IShare::TYPE_ROOM);
1497
				$share = $provider->getShareByToken($token);
1498
			} catch (ProviderException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1499
			} catch (ShareNotFound $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1500
			}
1501
		}
1502
1503
		if ($share === null) {
1504
			throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
1505
		}
1506
1507
		$this->checkExpireDate($share);
1508
1509
		/*
1510
		 * Reduce the permissions for link or email shares if public upload is not enabled
1511
		 */
1512
		if (($share->getShareType() === IShare::TYPE_LINK || $share->getShareType() === IShare::TYPE_EMAIL)
1513
			&& !$this->shareApiLinkAllowPublicUpload()) {
1514
			$share->setPermissions($share->getPermissions() & ~(\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE));
1515
		}
1516
1517
		return $share;
1518
	}
1519
1520
	protected function checkExpireDate($share) {
1521
		if ($share->isExpired()) {
1522
			$this->deleteShare($share);
1523
			throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
1524
		}
1525
	}
1526
1527
	/**
1528
	 * Verify the password of a public share
1529
	 *
1530
	 * @param IShare $share
1531
	 * @param string $password
1532
	 * @return bool
1533
	 */
1534
	public function checkPassword(IShare $share, $password) {
1535
		$passwordProtected = $share->getShareType() !== IShare::TYPE_LINK
1536
							 || $share->getShareType() !== IShare::TYPE_EMAIL
1537
							 || $share->getShareType() !== IShare::TYPE_CIRCLE;
1538
		if (!$passwordProtected) {
1539
			//TODO maybe exception?
1540
			return false;
1541
		}
1542
1543
		if ($password === null || $share->getPassword() === null) {
0 ignored issues
show
introduced by
The condition $share->getPassword() === null is always false.
Loading history...
1544
			return false;
1545
		}
1546
1547
		$newHash = '';
1548
		if (!$this->hasher->verify($password, $share->getPassword(), $newHash)) {
1549
			return false;
1550
		}
1551
1552
		if (!empty($newHash)) {
1553
			$share->setPassword($newHash);
1554
			$provider = $this->factory->getProviderForType($share->getShareType());
1555
			$provider->update($share);
1556
		}
1557
1558
		return true;
1559
	}
1560
1561
	/**
1562
	 * @inheritdoc
1563
	 */
1564
	public function userDeleted($uid) {
1565
		$types = [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK, IShare::TYPE_REMOTE, IShare::TYPE_EMAIL];
1566
1567
		foreach ($types as $type) {
1568
			try {
1569
				$provider = $this->factory->getProviderForType($type);
1570
			} catch (ProviderException $e) {
1571
				continue;
1572
			}
1573
			$provider->userDeleted($uid, $type);
1574
		}
1575
	}
1576
1577
	/**
1578
	 * @inheritdoc
1579
	 */
1580
	public function groupDeleted($gid) {
1581
		$provider = $this->factory->getProviderForType(IShare::TYPE_GROUP);
1582
		$provider->groupDeleted($gid);
1583
1584
		$excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', '');
1585
		if ($excludedGroups === '') {
1586
			return;
1587
		}
1588
1589
		$excludedGroups = json_decode($excludedGroups, true);
1590
		if (json_last_error() !== JSON_ERROR_NONE) {
1591
			return;
1592
		}
1593
1594
		$excludedGroups = array_diff($excludedGroups, [$gid]);
1595
		$this->config->setAppValue('core', 'shareapi_exclude_groups_list', json_encode($excludedGroups));
1596
	}
1597
1598
	/**
1599
	 * @inheritdoc
1600
	 */
1601
	public function userDeletedFromGroup($uid, $gid) {
1602
		$provider = $this->factory->getProviderForType(IShare::TYPE_GROUP);
1603
		$provider->userDeletedFromGroup($uid, $gid);
1604
	}
1605
1606
	/**
1607
	 * Get access list to a path. This means
1608
	 * all the users that can access a given path.
1609
	 *
1610
	 * Consider:
1611
	 * -root
1612
	 * |-folder1 (23)
1613
	 *  |-folder2 (32)
1614
	 *   |-fileA (42)
1615
	 *
1616
	 * fileA is shared with user1 and user1@server1
1617
	 * folder2 is shared with group2 (user4 is a member of group2)
1618
	 * folder1 is shared with user2 (renamed to "folder (1)") and user2@server2
1619
	 *
1620
	 * Then the access list to '/folder1/folder2/fileA' with $currentAccess is:
1621
	 * [
1622
	 *  users  => [
1623
	 *      'user1' => ['node_id' => 42, 'node_path' => '/fileA'],
1624
	 *      'user4' => ['node_id' => 32, 'node_path' => '/folder2'],
1625
	 *      'user2' => ['node_id' => 23, 'node_path' => '/folder (1)'],
1626
	 *  ],
1627
	 *  remote => [
1628
	 *      'user1@server1' => ['node_id' => 42, 'token' => 'SeCr3t'],
1629
	 *      'user2@server2' => ['node_id' => 23, 'token' => 'FooBaR'],
1630
	 *  ],
1631
	 *  public => bool
1632
	 *  mail => bool
1633
	 * ]
1634
	 *
1635
	 * The access list to '/folder1/folder2/fileA' **without** $currentAccess is:
1636
	 * [
1637
	 *  users  => ['user1', 'user2', 'user4'],
1638
	 *  remote => bool,
1639
	 *  public => bool
1640
	 *  mail => bool
1641
	 * ]
1642
	 *
1643
	 * This is required for encryption/activity
1644
	 *
1645
	 * @param \OCP\Files\Node $path
1646
	 * @param bool $recursive Should we check all parent folders as well
1647
	 * @param bool $currentAccess Ensure the recipient has access to the file (e.g. did not unshare it)
1648
	 * @return array
1649
	 */
1650
	public function getAccessList(\OCP\Files\Node $path, $recursive = true, $currentAccess = false) {
1651
		$owner = $path->getOwner();
1652
1653
		if ($owner === null) {
1654
			return [];
1655
		}
1656
1657
		$owner = $owner->getUID();
1658
1659
		if ($currentAccess) {
1660
			$al = ['users' => [], 'remote' => [], 'public' => false];
1661
		} else {
1662
			$al = ['users' => [], 'remote' => false, 'public' => false];
1663
		}
1664
		if (!$this->userManager->userExists($owner)) {
1665
			return $al;
1666
		}
1667
1668
		//Get node for the owner and correct the owner in case of external storages
1669
		$userFolder = $this->rootFolder->getUserFolder($owner);
1670
		if ($path->getId() !== $userFolder->getId() && !$userFolder->isSubNode($path)) {
1671
			$nodes = $userFolder->getById($path->getId());
1672
			$path = array_shift($nodes);
1673
			if ($path->getOwner() === null) {
1674
				return [];
1675
			}
1676
			$owner = $path->getOwner()->getUID();
1677
		}
1678
1679
		$providers = $this->factory->getAllProviders();
1680
1681
		/** @var Node[] $nodes */
1682
		$nodes = [];
1683
1684
1685
		if ($currentAccess) {
1686
			$ownerPath = $path->getPath();
1687
			$ownerPath = explode('/', $ownerPath, 4);
1688
			if (count($ownerPath) < 4) {
1689
				$ownerPath = '';
1690
			} else {
1691
				$ownerPath = $ownerPath[3];
1692
			}
1693
			$al['users'][$owner] = [
1694
				'node_id' => $path->getId(),
1695
				'node_path' => '/' . $ownerPath,
1696
			];
1697
		} else {
1698
			$al['users'][] = $owner;
1699
		}
1700
1701
		// Collect all the shares
1702
		while ($path->getPath() !== $userFolder->getPath()) {
1703
			$nodes[] = $path;
1704
			if (!$recursive) {
1705
				break;
1706
			}
1707
			$path = $path->getParent();
1708
		}
1709
1710
		foreach ($providers as $provider) {
1711
			$tmp = $provider->getAccessList($nodes, $currentAccess);
1712
1713
			foreach ($tmp as $k => $v) {
1714
				if (isset($al[$k])) {
1715
					if (is_array($al[$k])) {
1716
						if ($currentAccess) {
1717
							$al[$k] += $v;
1718
						} else {
1719
							$al[$k] = array_merge($al[$k], $v);
1720
							$al[$k] = array_unique($al[$k]);
1721
							$al[$k] = array_values($al[$k]);
1722
						}
1723
					} else {
1724
						$al[$k] = $al[$k] || $v;
1725
					}
1726
				} else {
1727
					$al[$k] = $v;
1728
				}
1729
			}
1730
		}
1731
1732
		return $al;
1733
	}
1734
1735
	/**
1736
	 * Create a new share
1737
	 *
1738
	 * @return IShare
1739
	 */
1740
	public function newShare() {
1741
		return new \OC\Share20\Share($this->rootFolder, $this->userManager);
1742
	}
1743
1744
	/**
1745
	 * Is the share API enabled
1746
	 *
1747
	 * @return bool
1748
	 */
1749
	public function shareApiEnabled() {
1750
		return $this->config->getAppValue('core', 'shareapi_enabled', 'yes') === 'yes';
1751
	}
1752
1753
	/**
1754
	 * Is public link sharing enabled
1755
	 *
1756
	 * @return bool
1757
	 */
1758
	public function shareApiAllowLinks() {
1759
		return $this->config->getAppValue('core', 'shareapi_allow_links', 'yes') === 'yes';
1760
	}
1761
1762
	/**
1763
	 * Is password on public link requires
1764
	 *
1765
	 * @return bool
1766
	 */
1767
	public function shareApiLinkEnforcePassword() {
1768
		return $this->config->getAppValue('core', 'shareapi_enforce_links_password', 'no') === 'yes';
1769
	}
1770
1771
	/**
1772
	 * Is default link expire date enabled
1773
	 *
1774
	 * @return bool
1775
	 */
1776
	public function shareApiLinkDefaultExpireDate() {
1777
		return $this->config->getAppValue('core', 'shareapi_default_expire_date', 'no') === 'yes';
1778
	}
1779
1780
	/**
1781
	 * Is default link expire date enforced
1782
	 *`
1783
	 * @return bool
1784
	 */
1785
	public function shareApiLinkDefaultExpireDateEnforced() {
1786
		return $this->shareApiLinkDefaultExpireDate() &&
1787
			$this->config->getAppValue('core', 'shareapi_enforce_expire_date', 'no') === 'yes';
1788
	}
1789
1790
1791
	/**
1792
	 * Number of default link expire days
1793
	 * @return int
1794
	 */
1795
	public function shareApiLinkDefaultExpireDays() {
1796
		return (int)$this->config->getAppValue('core', 'shareapi_expire_after_n_days', '7');
1797
	}
1798
1799
	/**
1800
	 * Is default internal expire date enabled
1801
	 *
1802
	 * @return bool
1803
	 */
1804
	public function shareApiInternalDefaultExpireDate(): bool {
1805
		return $this->config->getAppValue('core', 'shareapi_default_internal_expire_date', 'no') === 'yes';
1806
	}
1807
1808
	/**
1809
	 * Is default remote expire date enabled
1810
	 *
1811
	 * @return bool
1812
	 */
1813
	public function shareApiRemoteDefaultExpireDate(): bool {
1814
		return $this->config->getAppValue('core', 'shareapi_default_remote_expire_date', 'no') === 'yes';
1815
	}
1816
1817
	/**
1818
	 * Is default expire date enforced
1819
	 *
1820
	 * @return bool
1821
	 */
1822
	public function shareApiInternalDefaultExpireDateEnforced(): bool {
1823
		return $this->shareApiInternalDefaultExpireDate() &&
1824
			$this->config->getAppValue('core', 'shareapi_enforce_internal_expire_date', 'no') === 'yes';
1825
	}
1826
1827
	/**
1828
	 * Is default expire date enforced for remote shares
1829
	 *
1830
	 * @return bool
1831
	 */
1832
	public function shareApiRemoteDefaultExpireDateEnforced(): bool {
1833
		return $this->shareApiRemoteDefaultExpireDate() &&
1834
			$this->config->getAppValue('core', 'shareapi_enforce_remote_expire_date', 'no') === 'yes';
1835
	}
1836
1837
	/**
1838
	 * Number of default expire days
1839
	 * @return int
1840
	 */
1841
	public function shareApiInternalDefaultExpireDays(): int {
1842
		return (int)$this->config->getAppValue('core', 'shareapi_internal_expire_after_n_days', '7');
1843
	}
1844
1845
	/**
1846
	 * Number of default expire days for remote shares
1847
	 * @return int
1848
	 */
1849
	public function shareApiRemoteDefaultExpireDays(): int {
1850
		return (int)$this->config->getAppValue('core', 'shareapi_remote_expire_after_n_days', '7');
1851
	}
1852
1853
	/**
1854
	 * Allow public upload on link shares
1855
	 *
1856
	 * @return bool
1857
	 */
1858
	public function shareApiLinkAllowPublicUpload() {
1859
		return $this->config->getAppValue('core', 'shareapi_allow_public_upload', 'yes') === 'yes';
1860
	}
1861
1862
	/**
1863
	 * check if user can only share with group members
1864
	 * @return bool
1865
	 */
1866
	public function shareWithGroupMembersOnly() {
1867
		return $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
1868
	}
1869
1870
	/**
1871
	 * Check if users can share with groups
1872
	 * @return bool
1873
	 */
1874
	public function allowGroupSharing() {
1875
		return $this->config->getAppValue('core', 'shareapi_allow_group_sharing', 'yes') === 'yes';
1876
	}
1877
1878
	public function allowEnumeration(): bool {
1879
		return $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
1880
	}
1881
1882
	public function limitEnumerationToGroups(): bool {
1883
		return $this->allowEnumeration() &&
1884
			$this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
1885
	}
1886
1887
	public function limitEnumerationToPhone(): bool {
1888
		return $this->allowEnumeration() &&
1889
			$this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
1890
	}
1891
1892
	public function allowEnumerationFullMatch(): bool {
1893
		return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes';
1894
	}
1895
1896
	/**
1897
	 * Copied from \OC_Util::isSharingDisabledForUser
1898
	 *
1899
	 * TODO: Deprecate fuction from OC_Util
1900
	 *
1901
	 * @param string $userId
1902
	 * @return bool
1903
	 */
1904
	public function sharingDisabledForUser($userId) {
1905
		if ($userId === null) {
0 ignored issues
show
introduced by
The condition $userId === null is always false.
Loading history...
1906
			return false;
1907
		}
1908
1909
		if (isset($this->sharingDisabledForUsersCache[$userId])) {
1910
			return $this->sharingDisabledForUsersCache[$userId];
1911
		}
1912
1913
		if ($this->config->getAppValue('core', 'shareapi_exclude_groups', 'no') === 'yes') {
1914
			$groupsList = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', '');
1915
			$excludedGroups = json_decode($groupsList);
1916
			if (is_null($excludedGroups)) {
1917
				$excludedGroups = explode(',', $groupsList);
1918
				$newValue = json_encode($excludedGroups);
1919
				$this->config->setAppValue('core', 'shareapi_exclude_groups_list', $newValue);
1920
			}
1921
			$user = $this->userManager->get($userId);
1922
			$usersGroups = $this->groupManager->getUserGroupIds($user);
1923
			if (!empty($usersGroups)) {
1924
				$remainingGroups = array_diff($usersGroups, $excludedGroups);
1925
				// if the user is only in groups which are disabled for sharing then
1926
				// sharing is also disabled for the user
1927
				if (empty($remainingGroups)) {
1928
					$this->sharingDisabledForUsersCache[$userId] = true;
1929
					return true;
1930
				}
1931
			}
1932
		}
1933
1934
		$this->sharingDisabledForUsersCache[$userId] = false;
1935
		return false;
1936
	}
1937
1938
	/**
1939
	 * @inheritdoc
1940
	 */
1941
	public function outgoingServer2ServerSharesAllowed() {
1942
		return $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'yes';
1943
	}
1944
1945
	/**
1946
	 * @inheritdoc
1947
	 */
1948
	public function outgoingServer2ServerGroupSharesAllowed() {
1949
		return $this->config->getAppValue('files_sharing', 'outgoing_server2server_group_share_enabled', 'no') === 'yes';
1950
	}
1951
1952
	/**
1953
	 * @inheritdoc
1954
	 */
1955
	public function shareProviderExists($shareType) {
1956
		try {
1957
			$this->factory->getProviderForType($shareType);
1958
		} catch (ProviderException $e) {
1959
			return false;
1960
		}
1961
1962
		return true;
1963
	}
1964
1965
	public function registerShareProvider(string $shareProviderClass): void {
1966
		$this->factory->registerProvider($shareProviderClass);
1967
	}
1968
1969
	public function getAllShares(): iterable {
1970
		$providers = $this->factory->getAllProviders();
1971
1972
		foreach ($providers as $provider) {
1973
			yield from $provider->getAllShares();
1974
		}
1975
	}
1976
}
1977