Passed
Push — master ( 31cc07...56b08c )
by Joas
24:48 queued 11s
created

Manager::limitEnumerationToPhone()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

797
			$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
798
                            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

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

1027
			/** @scrutinizer ignore-call */ 
1028
   $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...
1028
		} else {
1029
			$share = $provider->update($share);
1030
		}
1031
1032
		if ($expirationDateUpdated === true) {
1033
			\OC_Hook::emit(Share::class, 'post_set_expiration_date', [
1034
				'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
1035
				'itemSource' => $share->getNode()->getId(),
1036
				'date' => $share->getExpirationDate(),
1037
				'uidOwner' => $share->getSharedBy(),
1038
			]);
1039
		}
1040
1041
		if ($share->getPassword() !== $originalShare->getPassword()) {
1042
			\OC_Hook::emit(Share::class, 'post_update_password', [
1043
				'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
1044
				'itemSource' => $share->getNode()->getId(),
1045
				'uidOwner' => $share->getSharedBy(),
1046
				'token' => $share->getToken(),
1047
				'disabled' => is_null($share->getPassword()),
1048
			]);
1049
		}
1050
1051
		if ($share->getPermissions() !== $originalShare->getPermissions()) {
1052
			if ($this->userManager->userExists($share->getShareOwner())) {
1053
				$userFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
1054
			} else {
1055
				$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
1056
			}
1057
			\OC_Hook::emit(Share::class, 'post_update_permissions', [
1058
				'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
1059
				'itemSource' => $share->getNode()->getId(),
1060
				'shareType' => $share->getShareType(),
1061
				'shareWith' => $share->getSharedWith(),
1062
				'uidOwner' => $share->getSharedBy(),
1063
				'permissions' => $share->getPermissions(),
1064
				'path' => $userFolder->getRelativePath($share->getNode()->getPath()),
1065
			]);
1066
		}
1067
1068
		return $share;
1069
	}
1070
1071
	/**
1072
	 * Accept a share.
1073
	 *
1074
	 * @param IShare $share
1075
	 * @param string $recipientId
1076
	 * @return IShare The share object
1077
	 * @throws \InvalidArgumentException
1078
	 * @since 9.0.0
1079
	 */
1080
	public function acceptShare(IShare $share, string $recipientId): IShare {
1081
		[$providerId, ] = $this->splitFullId($share->getFullId());
1082
		$provider = $this->factory->getProvider($providerId);
1083
1084
		if (!method_exists($provider, 'acceptShare')) {
1085
			// TODO FIX ME
1086
			throw new \InvalidArgumentException('Share provider does not support accepting');
1087
		}
1088
		$provider->acceptShare($share, $recipientId);
1089
		$event = new GenericEvent($share);
1090
		$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

1090
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
1091
                           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

1090
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ 'OCP\Share::postAcceptShare', $event);
Loading history...
1091
1092
		return $share;
1093
	}
1094
1095
	/**
1096
	 * Updates the password of the given share if it is not the same as the
1097
	 * password of the original share.
1098
	 *
1099
	 * @param IShare $share the share to update its password.
1100
	 * @param IShare $originalShare the original share to compare its
1101
	 *        password with.
1102
	 * @return boolean whether the password was updated or not.
1103
	 */
1104
	private function updateSharePasswordIfNeeded(IShare $share, IShare $originalShare) {
1105
		$passwordsAreDifferent = ($share->getPassword() !== $originalShare->getPassword()) &&
1106
									(($share->getPassword() !== null && $originalShare->getPassword() === null) ||
0 ignored issues
show
introduced by
The condition $originalShare->getPassword() === null is always false.
Loading history...
1107
									 ($share->getPassword() === null && $originalShare->getPassword() !== null) ||
0 ignored issues
show
introduced by
The condition $share->getPassword() === null is always false.
Loading history...
1108
									 ($share->getPassword() !== null && $originalShare->getPassword() !== null &&
1109
										!$this->hasher->verify($share->getPassword(), $originalShare->getPassword())));
1110
1111
		// Password updated.
1112
		if ($passwordsAreDifferent) {
1113
			//Verify the password
1114
			$this->verifyPassword($share->getPassword());
1115
1116
			// If a password is set. Hash it!
1117
			if ($share->getPassword() !== null) {
0 ignored issues
show
introduced by
The condition $share->getPassword() !== null is always true.
Loading history...
1118
				$share->setPassword($this->hasher->hash($share->getPassword()));
1119
1120
				return true;
1121
			}
1122
		} else {
1123
			// Reset the password to the original one, as it is either the same
1124
			// as the "new" password or a hashed version of it.
1125
			$share->setPassword($originalShare->getPassword());
1126
		}
1127
1128
		return false;
1129
	}
1130
1131
	/**
1132
	 * Delete all the children of this share
1133
	 * FIXME: remove once https://github.com/owncloud/core/pull/21660 is in
1134
	 *
1135
	 * @param IShare $share
1136
	 * @return IShare[] List of deleted shares
1137
	 */
1138
	protected function deleteChildren(IShare $share) {
1139
		$deletedShares = [];
1140
1141
		$provider = $this->factory->getProviderForType($share->getShareType());
1142
1143
		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

1143
		foreach ($provider->/** @scrutinizer ignore-call */ getChildren($share) as $child) {
Loading history...
1144
			$deletedChildren = $this->deleteChildren($child);
1145
			$deletedShares = array_merge($deletedShares, $deletedChildren);
1146
1147
			$provider->delete($child);
1148
			$this->dispatcher->dispatchTyped(new Share\Events\ShareDeletedEvent($child));
1149
			$deletedShares[] = $child;
1150
		}
1151
1152
		return $deletedShares;
1153
	}
1154
1155
	/**
1156
	 * Delete a share
1157
	 *
1158
	 * @param IShare $share
1159
	 * @throws ShareNotFound
1160
	 * @throws \InvalidArgumentException
1161
	 */
1162
	public function deleteShare(IShare $share) {
1163
		try {
1164
			$share->getFullId();
1165
		} catch (\UnexpectedValueException $e) {
1166
			throw new \InvalidArgumentException('Share does not have a full id');
1167
		}
1168
1169
		$event = new GenericEvent($share);
1170
		$this->legacyDispatcher->dispatch('OCP\Share::preUnshare', $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

1170
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
1171
                           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...
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

1170
		$this->legacyDispatcher->dispatch(/** @scrutinizer ignore-type */ 'OCP\Share::preUnshare', $event);
Loading history...
1171
1172
		// Get all children and delete them as well
1173
		$deletedShares = $this->deleteChildren($share);
1174
1175
		// Do the actual delete
1176
		$provider = $this->factory->getProviderForType($share->getShareType());
1177
		$provider->delete($share);
1178
1179
		$this->dispatcher->dispatchTyped(new Share\Events\ShareDeletedEvent($share));
1180
1181
		// All the deleted shares caused by this delete
1182
		$deletedShares[] = $share;
1183
1184
		// Emit post hook
1185
		$event->setArgument('deletedShares', $deletedShares);
1186
		$this->legacyDispatcher->dispatch('OCP\Share::postUnshare', $event);
1187
	}
1188
1189
1190
	/**
1191
	 * Unshare a file as the recipient.
1192
	 * This can be different from a regular delete for example when one of
1193
	 * the users in a groups deletes that share. But the provider should
1194
	 * handle this.
1195
	 *
1196
	 * @param IShare $share
1197
	 * @param string $recipientId
1198
	 */
1199
	public function deleteFromSelf(IShare $share, $recipientId) {
1200
		[$providerId, ] = $this->splitFullId($share->getFullId());
1201
		$provider = $this->factory->getProvider($providerId);
1202
1203
		$provider->deleteFromSelf($share, $recipientId);
1204
		$event = new GenericEvent($share);
1205
		$this->legacyDispatcher->dispatch('OCP\Share::postUnshareFromSelf', $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

1205
		$this->legacyDispatcher->/** @scrutinizer ignore-call */ 
1206
                           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...
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

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

1392
		$share = $provider->getShareById(/** @scrutinizer ignore-type */ $id, $recipient);
Loading history...
1393
1394
		$this->checkExpireDate($share);
1395
1396
		return $share;
1397
	}
1398
1399
	/**
1400
	 * Get all the shares for a given path
1401
	 *
1402
	 * @param \OCP\Files\Node $path
1403
	 * @param int $page
1404
	 * @param int $perPage
1405
	 *
1406
	 * @return Share[]
1407
	 */
1408
	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

1408
	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

1408
	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

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