Completed
Push — master ( a84fba...e8b4d4 )
by Thomas
11:12
created

ShareesController::isRemoteSharingAllowed()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 3
nop 1
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Björn Schießle <[email protected]>
4
 * @author Joas Schilling <[email protected]>
5
 * @author Roeland Jago Douma <[email protected]>
6
 * @author Thomas Müller <[email protected]>
7
 *
8
 * @copyright Copyright (c) 2016, ownCloud GmbH.
9
 * @license AGPL-3.0
10
 *
11
 * This code is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License, version 3,
13
 * as published by the Free Software Foundation.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License, version 3,
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
22
 *
23
 */
24
25
namespace OCA\Files_Sharing\Controller;
26
27
use OCP\AppFramework\Http;
28
use OCP\AppFramework\Http\DataResponse;
29
use OCP\AppFramework\OCSController;
30
use OCP\Contacts\IManager;
31
use OCP\IGroup;
32
use OCP\IGroupManager;
33
use OCP\ILogger;
34
use OCP\IRequest;
35
use OCP\IUser;
36
use OCP\IUserManager;
37
use OCP\IConfig;
38
use OCP\IUserSession;
39
use OCP\IURLGenerator;
40
use OCP\Share;
41
42
class ShareesController extends OCSController  {
43
44
	/** @var IGroupManager */
45
	protected $groupManager;
46
47
	/** @var IUserManager */
48
	protected $userManager;
49
50
	/** @var IManager */
51
	protected $contactsManager;
52
53
	/** @var IConfig */
54
	protected $config;
55
56
	/** @var IUserSession */
57
	protected $userSession;
58
59
	/** @var IRequest */
60
	protected $request;
61
62
	/** @var IURLGenerator */
63
	protected $urlGenerator;
64
65
	/** @var ILogger */
66
	protected $logger;
67
68
	/** @var \OCP\Share\IManager */
69
	protected $shareManager;
70
71
	/** @var bool */
72
	protected $shareWithGroupOnly = false;
73
74
	/** @var bool */
75
	protected $shareeEnumeration = true;
76
77
	/** @var int */
78
	protected $offset = 0;
79
80
	/** @var int */
81
	protected $limit = 10;
82
83
	/** @var array */
84
	protected $result = [
85
		'exact' => [
86
			'users' => [],
87
			'groups' => [],
88
			'remotes' => [],
89
		],
90
		'users' => [],
91
		'groups' => [],
92
		'remotes' => [],
93
	];
94
95
	protected $reachedEndFor = [];
96
97
	/**
98
	 * @param IGroupManager $groupManager
99
	 * @param IUserManager $userManager
100
	 * @param IManager $contactsManager
101
	 * @param IConfig $config
102
	 * @param IUserSession $userSession
103
	 * @param IURLGenerator $urlGenerator
104
	 * @param IRequest $request
105
	 * @param ILogger $logger
106
	 * @param \OCP\Share\IManager $shareManager
107
	 */
108 View Code Duplication
	public function __construct($appName,
109
			IRequest $request,
110
			IGroupManager $groupManager,
111
			IUserManager $userManager,
112
			IManager $contactsManager,
113
			IConfig $config,
114
			IUserSession $userSession,
115
			IURLGenerator $urlGenerator,
116
			ILogger $logger,
117
			\OCP\Share\IManager $shareManager) {
118
		parent::__construct($appName, $request);
119
120
		$this->groupManager = $groupManager;
121
		$this->userManager = $userManager;
122
		$this->contactsManager = $contactsManager;
123
		$this->config = $config;
124
		$this->userSession = $userSession;
125
		$this->urlGenerator = $urlGenerator;
126
		$this->request = $request;
127
		$this->logger = $logger;
128
		$this->shareManager = $shareManager;
129
	}
130
131
	/**
132
	 * @param string $search
133
	 */
134
	protected function getUsers($search) {
135
		$this->result['users'] = $this->result['exact']['users'] = $users = [];
136
137
		$userGroups = [];
138
		if ($this->shareWithGroupOnly) {
139
			// Search in all the groups this user is part of
140
			$userGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser());
0 ignored issues
show
Bug introduced by
It seems like $this->userSession->getUser() can be null; however, getUserGroupIds() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
141
			foreach ($userGroups as $userGroup) {
142
				$usersTmp = $this->groupManager->displayNamesInGroup($userGroup, $search, $this->limit, $this->offset);
143
				foreach ($usersTmp as $uid => $userDisplayName) {
144
					$users[$uid] = $userDisplayName;
145
				}
146
			}
147
		} else {
148
			// Search in all users
149
			$usersTmp = $this->userManager->searchDisplayName($search, $this->limit, $this->offset);
150
151
			foreach ($usersTmp as $user) {
152
				$users[$user->getUID()] = $user->getDisplayName();
153
			}
154
		}
155
156 View Code Duplication
		if (!$this->shareeEnumeration || sizeof($users) < $this->limit) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
157
			$this->reachedEndFor[] = 'users';
158
		}
159
160
		$foundUserById = false;
161
		foreach ($users as $uid => $userDisplayName) {
162
			if (strtolower($uid) === strtolower($search) || strtolower($userDisplayName) === strtolower($search)) {
163
				if (strtolower($uid) === strtolower($search)) {
164
					$foundUserById = true;
165
				}
166
				$this->result['exact']['users'][] = [
167
					'label' => $userDisplayName,
168
					'value' => [
169
						'shareType' => Share::SHARE_TYPE_USER,
170
						'shareWith' => $uid,
171
					],
172
				];
173 View Code Duplication
			} else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
174
				$this->result['users'][] = [
175
					'label' => $userDisplayName,
176
					'value' => [
177
						'shareType' => Share::SHARE_TYPE_USER,
178
						'shareWith' => $uid,
179
					],
180
				];
181
			}
182
		}
183
184
		if ($this->offset === 0 && !$foundUserById) {
185
			// On page one we try if the search result has a direct hit on the
186
			// user id and if so, we add that to the exact match list
187
			$user = $this->userManager->get($search);
188
			if ($user instanceof IUser) {
189
				$addUser = true;
190
191
				if ($this->shareWithGroupOnly) {
192
					// Only add, if we have a common group
193
					$commonGroups = array_intersect($userGroups, $this->groupManager->getUserGroupIds($user));
194
					$addUser = !empty($commonGroups);
195
				}
196
197
				if ($addUser) {
198
					array_push($this->result['exact']['users'], [
199
						'label' => $user->getDisplayName(),
200
						'value' => [
201
							'shareType' => Share::SHARE_TYPE_USER,
202
							'shareWith' => $user->getUID(),
203
						],
204
					]);
205
				}
206
			}
207
		}
208
209
		if (!$this->shareeEnumeration) {
210
			$this->result['users'] = [];
211
		}
212
	}
213
214
	/**
215
	 * @param string $search
216
	 */
217
	protected function getGroups($search) {
218
		$this->result['groups'] = $this->result['exact']['groups'] = [];
219
220
		$groups = $this->groupManager->search($search, $this->limit, $this->offset);
221
		$groups = array_map(function (IGroup $group) { return $group->getGID(); }, $groups);
222
223 View Code Duplication
		if (!$this->shareeEnumeration || sizeof($groups) < $this->limit) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
224
			$this->reachedEndFor[] = 'groups';
225
		}
226
227
		$userGroups =  [];
228
		if (!empty($groups) && $this->shareWithGroupOnly) {
229
			// Intersect all the groups that match with the groups this user is a member of
230
			$userGroups = $this->groupManager->getUserGroups($this->userSession->getUser());
231
			$userGroups = array_map(function (IGroup $group) { return $group->getGID(); }, $userGroups);
232
			$groups = array_intersect($groups, $userGroups);
233
		}
234
235
		foreach ($groups as $gid) {
236
			if (strtolower($gid) === strtolower($search)) {
237
				$this->result['exact']['groups'][] = [
238
					'label' => $gid,
239
					'value' => [
240
						'shareType' => Share::SHARE_TYPE_GROUP,
241
						'shareWith' => $gid,
242
					],
243
				];
244 View Code Duplication
			} else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
245
				$this->result['groups'][] = [
246
					'label' => $gid,
247
					'value' => [
248
						'shareType' => Share::SHARE_TYPE_GROUP,
249
						'shareWith' => $gid,
250
					],
251
				];
252
			}
253
		}
254
255
		if ($this->offset === 0 && empty($this->result['exact']['groups'])) {
256
			// On page one we try if the search result has a direct hit on the
257
			// user id and if so, we add that to the exact match list
258
			$group = $this->groupManager->get($search);
259
			if ($group instanceof IGroup && (!$this->shareWithGroupOnly || in_array($group->getGID(), $userGroups))) {
260
				array_push($this->result['exact']['groups'], [
261
					'label' => $group->getGID(),
262
					'value' => [
263
						'shareType' => Share::SHARE_TYPE_GROUP,
264
						'shareWith' => $group->getGID(),
265
					],
266
				]);
267
			}
268
		}
269
270
		if (!$this->shareeEnumeration) {
271
			$this->result['groups'] = [];
272
		}
273
	}
274
275
	/**
276
	 * @param string $search
277
	 * @return array possible sharees
278
	 */
279
	protected function getRemote($search) {
280
		$this->result['remotes'] = [];
281
282
		// Search in contacts
283
		//@todo Pagination missing
284
		$addressBookContacts = $this->contactsManager->search($search, ['CLOUD', 'FN']);
285
		$foundRemoteById = false;
286
		foreach ($addressBookContacts as $contact) {
287
			if (isset($contact['isLocalSystemBook'])) {
288
				continue;
289
			}
290
			if (isset($contact['CLOUD'])) {
291
				$cloudIds = $contact['CLOUD'];
292
				if (!is_array($cloudIds)) {
293
					$cloudIds = [$cloudIds];
294
				}
295
				foreach ($cloudIds as $cloudId) {
296
					list(, $serverUrl) = $this->splitUserRemote($cloudId);
297
					if (strtolower($contact['FN']) === strtolower($search) || strtolower($cloudId) === strtolower($search)) {
298
						if (strtolower($cloudId) === strtolower($search)) {
299
							$foundRemoteById = true;
300
						}
301
						$this->result['exact']['remotes'][] = [
302
							'label' => $contact['FN'],
303
							'value' => [
304
								'shareType' => Share::SHARE_TYPE_REMOTE,
305
								'shareWith' => $cloudId,
306
								'server' => $serverUrl,
307
							],
308
						];
309 View Code Duplication
					} else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
310
						$this->result['remotes'][] = [
311
							'label' => $contact['FN'],
312
							'value' => [
313
								'shareType' => Share::SHARE_TYPE_REMOTE,
314
								'shareWith' => $cloudId,
315
								'server' => $serverUrl,
316
							],
317
						];
318
					}
319
				}
320
			}
321
		}
322
323
		if (!$this->shareeEnumeration) {
324
			$this->result['remotes'] = [];
325
		}
326
327
		if (!$foundRemoteById && substr_count($search, '@') >= 1 && $this->offset === 0) {
328
			$this->result['exact']['remotes'][] = [
329
				'label' => $search,
330
				'value' => [
331
					'shareType' => Share::SHARE_TYPE_REMOTE,
332
					'shareWith' => $search,
333
				],
334
			];
335
		}
336
337
		$this->reachedEndFor[] = 'remotes';
338
	}
339
340
	/**
341
	 * split user and remote from federated cloud id
342
	 *
343
	 * @param string $address federated share address
344
	 * @return array [user, remoteURL]
345
	 * @throws \Exception
346
	 */
347
	public function splitUserRemote($address) {
348
		if (strpos($address, '@') === false) {
349
			throw new \Exception('Invalid Federated Cloud ID');
350
		}
351
352
		// Find the first character that is not allowed in user names
353
		$id = str_replace('\\', '/', $address);
354
		$posSlash = strpos($id, '/');
355
		$posColon = strpos($id, ':');
356
357 View Code Duplication
		if ($posSlash === false && $posColon === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
358
			$invalidPos = strlen($id);
359
		} else if ($posSlash === false) {
360
			$invalidPos = $posColon;
361
		} else if ($posColon === false) {
362
			$invalidPos = $posSlash;
363
		} else {
364
			$invalidPos = min($posSlash, $posColon);
365
		}
366
367
		// Find the last @ before $invalidPos
368
		$pos = $lastAtPos = 0;
369 View Code Duplication
		while ($lastAtPos !== false && $lastAtPos <= $invalidPos) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
370
			$pos = $lastAtPos;
371
			$lastAtPos = strpos($id, '@', $pos + 1);
372
		}
373
374 View Code Duplication
		if ($pos !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
375
			$user = substr($id, 0, $pos);
376
			$remote = substr($id, $pos + 1);
377
			$remote = $this->fixRemoteURL($remote);
378
			if (!empty($user) && !empty($remote)) {
379
				return [$user, $remote];
380
			}
381
		}
382
383
		throw new \Exception('Invalid Federated Cloud ID');
384
	}
385
386
	/**
387
	 * Strips away a potential file names and trailing slashes:
388
	 * - http://localhost
389
	 * - http://localhost/
390
	 * - http://localhost/index.php
391
	 * - http://localhost/index.php/s/{shareToken}
392
	 *
393
	 * all return: http://localhost
394
	 *
395
	 * @param string $remote
396
	 * @return string
397
	 */
398 View Code Duplication
	protected function fixRemoteURL($remote) {
399
		$remote = str_replace('\\', '/', $remote);
400
		if ($fileNamePosition = strpos($remote, '/index.php')) {
401
			$remote = substr($remote, 0, $fileNamePosition);
402
		}
403
		$remote = rtrim($remote, '/');
404
405
		return $remote;
406
	}
407
408
	/**
409
	 * @NoAdminRequired
410
	 * @NoCSRFRequired
411
	 *
412
	 * @param string $search
413
	 * @param string $itemType
414
	 * @param int $page
415
	 * @param int $perPage
416
	 * @return array|DataResponse
417
	 */
418
	public function search($search = '', $itemType = null, $page = 1, $perPage = 200) {
419
420
		if ($perPage <= 0) {
421
			return [ 'statuscode' => Http::STATUS_BAD_REQUEST,
422
				'message' => 'Invalid perPage argument'];
423
		}
424
		if ($page <= 0) {
425
			return [ 'statuscode' => Http::STATUS_BAD_REQUEST,
426
				'message' => 'Invalid page'];
427
		}
428
429
		$shareTypes = [
430
			Share::SHARE_TYPE_USER,
431
		];
432
433
		if ($this->shareManager->allowGroupSharing()) {
434
			$shareTypes[] = Share::SHARE_TYPE_GROUP;
435
		}
436
437
		$shareTypes[] = Share::SHARE_TYPE_REMOTE;
438
439
		if (isset($_GET['shareType']) && is_array($_GET['shareType'])) {
440
			$shareTypes = array_intersect($shareTypes, $_GET['shareType']);
441
			sort($shareTypes);
442
443
		} else if (isset($_GET['shareType']) && is_numeric($_GET['shareType'])) {
444
			$shareTypes = array_intersect($shareTypes, [(int) $_GET['shareType']]);
445
			sort($shareTypes);
446
		}
447
448
		if (in_array(Share::SHARE_TYPE_REMOTE, $shareTypes) && !$this->isRemoteSharingAllowed($itemType)) {
449
			// Remove remote shares from type array, because it is not allowed.
450
			$shareTypes = array_diff($shareTypes, [Share::SHARE_TYPE_REMOTE]);
451
		}
452
453
		$this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
454
		$this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
455
		$this->limit = (int) $perPage;
456
		$this->offset = $perPage * ($page - 1);
457
458
		return $this->searchSharees($search, $itemType, $shareTypes, $page, $perPage);
459
	}
460
461
	/**
462
	 * Method to get out the static call for better testing
463
	 *
464
	 * @param string $itemType
465
	 * @return bool
466
	 */
467
	protected function isRemoteSharingAllowed($itemType) {
468
		try {
469
			$backend = Share::getBackend($itemType);
470
			return $backend->isShareTypeAllowed(Share::SHARE_TYPE_REMOTE);
471
		} catch (\Exception $e) {
472
			return false;
473
		}
474
	}
475
476
	/**
477
	 * Testable search function that does not need globals
478
	 *
479
	 * @param string $search
480
	 * @param string $itemType
481
	 * @param array $shareTypes
482
	 * @param int $page
483
	 * @param int $perPage
484
	 * @return DataResponse|array
485
	 */
486
	protected function searchSharees($search, $itemType, array $shareTypes, $page, $perPage) {
487
		// Verify arguments
488
		if ($itemType === null) {
489
			return [ 'statuscode' => Http::STATUS_BAD_REQUEST,
490
				'message' => 'Missing itemType'];
491
		}
492
493
		// Get users
494
		if (in_array(Share::SHARE_TYPE_USER, $shareTypes)) {
495
			$this->getUsers($search);
496
		}
497
498
		// Get groups
499
		if (in_array(Share::SHARE_TYPE_GROUP, $shareTypes)) {
500
			$this->getGroups($search);
501
		}
502
503
		// Get remote
504
		if (in_array(Share::SHARE_TYPE_REMOTE, $shareTypes)) {
505
			$this->getRemote($search);
506
		}
507
508
		$response = new DataResponse(['data' => $this->result]);
509
510
		if (sizeof($this->reachedEndFor) < 3) {
511
			$response->addHeader('Link', $this->getPaginationLink($page, [
512
				'search' => $search,
513
				'itemType' => $itemType,
514
				'shareType' => $shareTypes,
515
				'perPage' => $perPage,
516
			]));
517
		}
518
519
		return $response;
520
	}
521
522
	/**
523
	 * Generates a bunch of pagination links for the current page
524
	 *
525
	 * @param int $page Current page
526
	 * @param array $params Parameters for the URL
527
	 * @return string
528
	 */
529
	protected function getPaginationLink($page, array $params) {
530
		if ($this->isV2()) {
531
			$url = $this->urlGenerator->getAbsoluteURL('/ocs/v2.php/apps/files_sharing/api/v1/sharees') . '?';
532
		} else {
533
			$url = $this->urlGenerator->getAbsoluteURL('/ocs/v1.php/apps/files_sharing/api/v1/sharees') . '?';
534
		}
535
		$params['page'] = $page + 1;
536
		$link = '<' . $url . http_build_query($params) . '>; rel="next"';
537
538
		return $link;
539
	}
540
541
	/**
542
	 * @return bool
543
	 */
544
	protected function isV2() {
545
		return $this->request->getScriptName() === '/ocs/v2.php';
546
	}
547
}
548