Completed
Pull Request — master (#9293)
by Blizzz
31:50 queued 12:06
created
apps/user_ldap/lib/Group_LDAP.php 1 patch
Indentation   +1140 added lines, -1140 removed lines patch added patch discarded remove patch
@@ -46,1145 +46,1145 @@
 block discarded – undo
46 46
 use OCP\ILogger;
47 47
 
48 48
 class Group_LDAP extends BackendUtility implements \OCP\GroupInterface, IGroupLDAP {
49
-	protected $enabled = false;
50
-
51
-	/**
52
-	 * @var string[] $cachedGroupMembers array of users with gid as key
53
-	 */
54
-	protected $cachedGroupMembers;
55
-
56
-	/**
57
-	 * @var string[] $cachedGroupsByMember array of groups with uid as key
58
-	 */
59
-	protected $cachedGroupsByMember;
60
-
61
-	/** @var GroupPluginManager */
62
-	protected $groupPluginManager;
63
-
64
-	public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
65
-		parent::__construct($access);
66
-		$filter = $this->access->connection->ldapGroupFilter;
67
-		$gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
68
-		if(!empty($filter) && !empty($gassoc)) {
69
-			$this->enabled = true;
70
-		}
71
-
72
-		$this->cachedGroupMembers = new CappedMemoryCache();
73
-		$this->cachedGroupsByMember = new CappedMemoryCache();
74
-		$this->groupPluginManager = $groupPluginManager;
75
-	}
76
-
77
-	/**
78
-	 * is user in group?
79
-	 * @param string $uid uid of the user
80
-	 * @param string $gid gid of the group
81
-	 * @return bool
82
-	 *
83
-	 * Checks whether the user is member of a group or not.
84
-	 */
85
-	public function inGroup($uid, $gid) {
86
-		if(!$this->enabled) {
87
-			return false;
88
-		}
89
-		$cacheKey = 'inGroup'.$uid.':'.$gid;
90
-		$inGroup = $this->access->connection->getFromCache($cacheKey);
91
-		if(!is_null($inGroup)) {
92
-			return (bool)$inGroup;
93
-		}
94
-
95
-		$userDN = $this->access->username2dn($uid);
96
-
97
-		if(isset($this->cachedGroupMembers[$gid])) {
98
-			$isInGroup = in_array($userDN, $this->cachedGroupMembers[$gid]);
99
-			return $isInGroup;
100
-		}
101
-
102
-		$cacheKeyMembers = 'inGroup-members:'.$gid;
103
-		$members = $this->access->connection->getFromCache($cacheKeyMembers);
104
-		if(!is_null($members)) {
105
-			$this->cachedGroupMembers[$gid] = $members;
106
-			$isInGroup = in_array($userDN, $members);
107
-			$this->access->connection->writeToCache($cacheKey, $isInGroup);
108
-			return $isInGroup;
109
-		}
110
-
111
-		$groupDN = $this->access->groupname2dn($gid);
112
-		// just in case
113
-		if(!$groupDN || !$userDN) {
114
-			$this->access->connection->writeToCache($cacheKey, false);
115
-			return false;
116
-		}
117
-
118
-		//check primary group first
119
-		if($gid === $this->getUserPrimaryGroup($userDN)) {
120
-			$this->access->connection->writeToCache($cacheKey, true);
121
-			return true;
122
-		}
123
-
124
-		//usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
125
-		$members = $this->_groupMembers($groupDN);
126
-		$members = array_keys($members); // uids are returned as keys
127
-		if(!is_array($members) || count($members) === 0) {
128
-			$this->access->connection->writeToCache($cacheKey, false);
129
-			return false;
130
-		}
131
-
132
-		//extra work if we don't get back user DNs
133
-		if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
134
-			$dns = array();
135
-			$filterParts = array();
136
-			$bytes = 0;
137
-			foreach($members as $mid) {
138
-				$filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
139
-				$filterParts[] = $filter;
140
-				$bytes += strlen($filter);
141
-				if($bytes >= 9000000) {
142
-					// AD has a default input buffer of 10 MB, we do not want
143
-					// to take even the chance to exceed it
144
-					$filter = $this->access->combineFilterWithOr($filterParts);
145
-					$bytes = 0;
146
-					$filterParts = array();
147
-					$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
148
-					$dns = array_merge($dns, $users);
149
-				}
150
-			}
151
-			if(count($filterParts) > 0) {
152
-				$filter = $this->access->combineFilterWithOr($filterParts);
153
-				$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
154
-				$dns = array_merge($dns, $users);
155
-			}
156
-			$members = $dns;
157
-		}
158
-
159
-		$isInGroup = in_array($userDN, $members);
160
-		$this->access->connection->writeToCache($cacheKey, $isInGroup);
161
-		$this->access->connection->writeToCache($cacheKeyMembers, $members);
162
-		$this->cachedGroupMembers[$gid] = $members;
163
-
164
-		return $isInGroup;
165
-	}
166
-
167
-	/**
168
-	 * @param string $dnGroup
169
-	 * @return array
170
-	 *
171
-	 * For a group that has user membership defined by an LDAP search url attribute returns the users
172
-	 * that match the search url otherwise returns an empty array.
173
-	 */
174
-	public function getDynamicGroupMembers($dnGroup) {
175
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
176
-
177
-		if (empty($dynamicGroupMemberURL)) {
178
-			return array();
179
-		}
180
-
181
-		$dynamicMembers = array();
182
-		$memberURLs = $this->access->readAttribute(
183
-			$dnGroup,
184
-			$dynamicGroupMemberURL,
185
-			$this->access->connection->ldapGroupFilter
186
-		);
187
-		if ($memberURLs !== false) {
188
-			// this group has the 'memberURL' attribute so this is a dynamic group
189
-			// example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
190
-			// example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
191
-			$pos = strpos($memberURLs[0], '(');
192
-			if ($pos !== false) {
193
-				$memberUrlFilter = substr($memberURLs[0], $pos);
194
-				$foundMembers = $this->access->searchUsers($memberUrlFilter,'dn');
195
-				$dynamicMembers = array();
196
-				foreach($foundMembers as $value) {
197
-					$dynamicMembers[$value['dn'][0]] = 1;
198
-				}
199
-			} else {
200
-				\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
201
-					'of group ' . $dnGroup, ILogger::DEBUG);
202
-			}
203
-		}
204
-		return $dynamicMembers;
205
-	}
206
-
207
-	/**
208
-	 * @param string $dnGroup
209
-	 * @param array|null &$seen
210
-	 * @return array|mixed|null
211
-	 * @throws \OC\ServerNotAvailableException
212
-	 */
213
-	private function _groupMembers($dnGroup, &$seen = null) {
214
-		if ($seen === null) {
215
-			$seen = array();
216
-		}
217
-		$allMembers = array();
218
-		if (array_key_exists($dnGroup, $seen)) {
219
-			// avoid loops
220
-			return array();
221
-		}
222
-		// used extensively in cron job, caching makes sense for nested groups
223
-		$cacheKey = '_groupMembers'.$dnGroup;
224
-		$groupMembers = $this->access->connection->getFromCache($cacheKey);
225
-		if($groupMembers !== null) {
226
-			return $groupMembers;
227
-		}
228
-		$seen[$dnGroup] = 1;
229
-		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr,
230
-												$this->access->connection->ldapGroupFilter);
231
-		if (is_array($members)) {
232
-			foreach ($members as $member) {
233
-				$allMembers[$member] = 1;
234
-				$nestedGroups = $this->access->connection->ldapNestedGroups;
235
-				if (!empty($nestedGroups)) {
236
-					$subMembers = $this->_groupMembers($member, $seen);
237
-					if ($subMembers) {
238
-						$allMembers += $subMembers;
239
-					}
240
-				}
241
-			}
242
-		}
243
-
244
-		$allMembers += $this->getDynamicGroupMembers($dnGroup);
245
-
246
-		$this->access->connection->writeToCache($cacheKey, $allMembers);
247
-		return $allMembers;
248
-	}
249
-
250
-	/**
251
-	 * @param string $DN
252
-	 * @param array|null &$seen
253
-	 * @return array
254
-	 */
255
-	private function _getGroupDNsFromMemberOf($DN, &$seen = null) {
256
-		if ($seen === null) {
257
-			$seen = array();
258
-		}
259
-		if (array_key_exists($DN, $seen)) {
260
-			// avoid loops
261
-			return array();
262
-		}
263
-		$seen[$DN] = 1;
264
-		$groups = $this->access->readAttribute($DN, 'memberOf');
265
-		if (!is_array($groups)) {
266
-			return array();
267
-		}
268
-		$groups = $this->access->groupsMatchFilter($groups);
269
-		$allGroups =  $groups;
270
-		$nestedGroups = $this->access->connection->ldapNestedGroups;
271
-		if ((int)$nestedGroups === 1) {
272
-			foreach ($groups as $group) {
273
-				$subGroups = $this->_getGroupDNsFromMemberOf($group, $seen);
274
-				$allGroups = array_merge($allGroups, $subGroups);
275
-			}
276
-		}
277
-		return $allGroups;
278
-	}
279
-
280
-	/**
281
-	 * translates a gidNumber into an ownCloud internal name
282
-	 * @param string $gid as given by gidNumber on POSIX LDAP
283
-	 * @param string $dn a DN that belongs to the same domain as the group
284
-	 * @return string|bool
285
-	 */
286
-	public function gidNumber2Name($gid, $dn) {
287
-		$cacheKey = 'gidNumberToName' . $gid;
288
-		$groupName = $this->access->connection->getFromCache($cacheKey);
289
-		if(!is_null($groupName) && isset($groupName)) {
290
-			return $groupName;
291
-		}
292
-
293
-		//we need to get the DN from LDAP
294
-		$filter = $this->access->combineFilterWithAnd([
295
-			$this->access->connection->ldapGroupFilter,
296
-			'objectClass=posixGroup',
297
-			$this->access->connection->ldapGidNumber . '=' . $gid
298
-		]);
299
-		$result = $this->access->searchGroups($filter, array('dn'), 1);
300
-		if(empty($result)) {
301
-			return false;
302
-		}
303
-		$dn = $result[0]['dn'][0];
304
-
305
-		//and now the group name
306
-		//NOTE once we have separate ownCloud group IDs and group names we can
307
-		//directly read the display name attribute instead of the DN
308
-		$name = $this->access->dn2groupname($dn);
309
-
310
-		$this->access->connection->writeToCache($cacheKey, $name);
311
-
312
-		return $name;
313
-	}
314
-
315
-	/**
316
-	 * returns the entry's gidNumber
317
-	 * @param string $dn
318
-	 * @param string $attribute
319
-	 * @return string|bool
320
-	 */
321
-	private function getEntryGidNumber($dn, $attribute) {
322
-		$value = $this->access->readAttribute($dn, $attribute);
323
-		if(is_array($value) && !empty($value)) {
324
-			return $value[0];
325
-		}
326
-		return false;
327
-	}
328
-
329
-	/**
330
-	 * returns the group's primary ID
331
-	 * @param string $dn
332
-	 * @return string|bool
333
-	 */
334
-	public function getGroupGidNumber($dn) {
335
-		return $this->getEntryGidNumber($dn, 'gidNumber');
336
-	}
337
-
338
-	/**
339
-	 * returns the user's gidNumber
340
-	 * @param string $dn
341
-	 * @return string|bool
342
-	 */
343
-	public function getUserGidNumber($dn) {
344
-		$gidNumber = false;
345
-		if($this->access->connection->hasGidNumber) {
346
-			$gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
347
-			if($gidNumber === false) {
348
-				$this->access->connection->hasGidNumber = false;
349
-			}
350
-		}
351
-		return $gidNumber;
352
-	}
353
-
354
-	/**
355
-	 * returns a filter for a "users has specific gid" search or count operation
356
-	 *
357
-	 * @param string $groupDN
358
-	 * @param string $search
359
-	 * @return string
360
-	 * @throws \Exception
361
-	 */
362
-	private function prepareFilterForUsersHasGidNumber($groupDN, $search = '') {
363
-		$groupID = $this->getGroupGidNumber($groupDN);
364
-		if($groupID === false) {
365
-			throw new \Exception('Not a valid group');
366
-		}
367
-
368
-		$filterParts = [];
369
-		$filterParts[] = $this->access->getFilterForUserCount();
370
-		if ($search !== '') {
371
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
372
-		}
373
-		$filterParts[] = $this->access->connection->ldapGidNumber .'=' . $groupID;
374
-
375
-		return $this->access->combineFilterWithAnd($filterParts);
376
-	}
377
-
378
-	/**
379
-	 * returns a list of users that have the given group as gid number
380
-	 *
381
-	 * @param string $groupDN
382
-	 * @param string $search
383
-	 * @param int $limit
384
-	 * @param int $offset
385
-	 * @return string[]
386
-	 */
387
-	public function getUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
388
-		try {
389
-			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
390
-			$users = $this->access->fetchListOfUsers(
391
-				$filter,
392
-				[$this->access->connection->ldapUserDisplayName, 'dn'],
393
-				$limit,
394
-				$offset
395
-			);
396
-			return $this->access->nextcloudUserNames($users);
397
-		} catch (\Exception $e) {
398
-			return [];
399
-		}
400
-	}
401
-
402
-	/**
403
-	 * returns the number of users that have the given group as gid number
404
-	 *
405
-	 * @param string $groupDN
406
-	 * @param string $search
407
-	 * @param int $limit
408
-	 * @param int $offset
409
-	 * @return int
410
-	 */
411
-	public function countUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
412
-		try {
413
-			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
414
-			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
415
-			return (int)$users;
416
-		} catch (\Exception $e) {
417
-			return 0;
418
-		}
419
-	}
420
-
421
-	/**
422
-	 * gets the gidNumber of a user
423
-	 * @param string $dn
424
-	 * @return string
425
-	 */
426
-	public function getUserGroupByGid($dn) {
427
-		$groupID = $this->getUserGidNumber($dn);
428
-		if($groupID !== false) {
429
-			$groupName = $this->gidNumber2Name($groupID, $dn);
430
-			if($groupName !== false) {
431
-				return $groupName;
432
-			}
433
-		}
434
-
435
-		return false;
436
-	}
437
-
438
-	/**
439
-	 * translates a primary group ID into an Nextcloud internal name
440
-	 * @param string $gid as given by primaryGroupID on AD
441
-	 * @param string $dn a DN that belongs to the same domain as the group
442
-	 * @return string|bool
443
-	 */
444
-	public function primaryGroupID2Name($gid, $dn) {
445
-		$cacheKey = 'primaryGroupIDtoName';
446
-		$groupNames = $this->access->connection->getFromCache($cacheKey);
447
-		if(!is_null($groupNames) && isset($groupNames[$gid])) {
448
-			return $groupNames[$gid];
449
-		}
450
-
451
-		$domainObjectSid = $this->access->getSID($dn);
452
-		if($domainObjectSid === false) {
453
-			return false;
454
-		}
455
-
456
-		//we need to get the DN from LDAP
457
-		$filter = $this->access->combineFilterWithAnd(array(
458
-			$this->access->connection->ldapGroupFilter,
459
-			'objectsid=' . $domainObjectSid . '-' . $gid
460
-		));
461
-		$result = $this->access->searchGroups($filter, array('dn'), 1);
462
-		if(empty($result)) {
463
-			return false;
464
-		}
465
-		$dn = $result[0]['dn'][0];
466
-
467
-		//and now the group name
468
-		//NOTE once we have separate Nextcloud group IDs and group names we can
469
-		//directly read the display name attribute instead of the DN
470
-		$name = $this->access->dn2groupname($dn);
471
-
472
-		$this->access->connection->writeToCache($cacheKey, $name);
473
-
474
-		return $name;
475
-	}
476
-
477
-	/**
478
-	 * returns the entry's primary group ID
479
-	 * @param string $dn
480
-	 * @param string $attribute
481
-	 * @return string|bool
482
-	 */
483
-	private function getEntryGroupID($dn, $attribute) {
484
-		$value = $this->access->readAttribute($dn, $attribute);
485
-		if(is_array($value) && !empty($value)) {
486
-			return $value[0];
487
-		}
488
-		return false;
489
-	}
490
-
491
-	/**
492
-	 * returns the group's primary ID
493
-	 * @param string $dn
494
-	 * @return string|bool
495
-	 */
496
-	public function getGroupPrimaryGroupID($dn) {
497
-		return $this->getEntryGroupID($dn, 'primaryGroupToken');
498
-	}
499
-
500
-	/**
501
-	 * returns the user's primary group ID
502
-	 * @param string $dn
503
-	 * @return string|bool
504
-	 */
505
-	public function getUserPrimaryGroupIDs($dn) {
506
-		$primaryGroupID = false;
507
-		if($this->access->connection->hasPrimaryGroups) {
508
-			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
509
-			if($primaryGroupID === false) {
510
-				$this->access->connection->hasPrimaryGroups = false;
511
-			}
512
-		}
513
-		return $primaryGroupID;
514
-	}
515
-
516
-	/**
517
-	 * returns a filter for a "users in primary group" search or count operation
518
-	 *
519
-	 * @param string $groupDN
520
-	 * @param string $search
521
-	 * @return string
522
-	 * @throws \Exception
523
-	 */
524
-	private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
525
-		$groupID = $this->getGroupPrimaryGroupID($groupDN);
526
-		if($groupID === false) {
527
-			throw new \Exception('Not a valid group');
528
-		}
529
-
530
-		$filterParts = [];
531
-		$filterParts[] = $this->access->getFilterForUserCount();
532
-		if ($search !== '') {
533
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
534
-		}
535
-		$filterParts[] = 'primaryGroupID=' . $groupID;
536
-
537
-		return $this->access->combineFilterWithAnd($filterParts);
538
-	}
539
-
540
-	/**
541
-	 * returns a list of users that have the given group as primary group
542
-	 *
543
-	 * @param string $groupDN
544
-	 * @param string $search
545
-	 * @param int $limit
546
-	 * @param int $offset
547
-	 * @return string[]
548
-	 */
549
-	public function getUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
550
-		try {
551
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
552
-			$users = $this->access->fetchListOfUsers(
553
-				$filter,
554
-				array($this->access->connection->ldapUserDisplayName, 'dn'),
555
-				$limit,
556
-				$offset
557
-			);
558
-			return $this->access->nextcloudUserNames($users);
559
-		} catch (\Exception $e) {
560
-			return array();
561
-		}
562
-	}
563
-
564
-	/**
565
-	 * returns the number of users that have the given group as primary group
566
-	 *
567
-	 * @param string $groupDN
568
-	 * @param string $search
569
-	 * @param int $limit
570
-	 * @param int $offset
571
-	 * @return int
572
-	 */
573
-	public function countUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
574
-		try {
575
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
576
-			$users = $this->access->countUsers($filter, array('dn'), $limit, $offset);
577
-			return (int)$users;
578
-		} catch (\Exception $e) {
579
-			return 0;
580
-		}
581
-	}
582
-
583
-	/**
584
-	 * gets the primary group of a user
585
-	 * @param string $dn
586
-	 * @return string
587
-	 */
588
-	public function getUserPrimaryGroup($dn) {
589
-		$groupID = $this->getUserPrimaryGroupIDs($dn);
590
-		if($groupID !== false) {
591
-			$groupName = $this->primaryGroupID2Name($groupID, $dn);
592
-			if($groupName !== false) {
593
-				return $groupName;
594
-			}
595
-		}
596
-
597
-		return false;
598
-	}
599
-
600
-	/**
601
-	 * Get all groups a user belongs to
602
-	 * @param string $uid Name of the user
603
-	 * @return array with group names
604
-	 *
605
-	 * This function fetches all groups a user belongs to. It does not check
606
-	 * if the user exists at all.
607
-	 *
608
-	 * This function includes groups based on dynamic group membership.
609
-	 */
610
-	public function getUserGroups($uid) {
611
-		if(!$this->enabled) {
612
-			return array();
613
-		}
614
-		$cacheKey = 'getUserGroups'.$uid;
615
-		$userGroups = $this->access->connection->getFromCache($cacheKey);
616
-		if(!is_null($userGroups)) {
617
-			return $userGroups;
618
-		}
619
-		$userDN = $this->access->username2dn($uid);
620
-		if(!$userDN) {
621
-			$this->access->connection->writeToCache($cacheKey, array());
622
-			return array();
623
-		}
624
-
625
-		$groups = [];
626
-		$primaryGroup = $this->getUserPrimaryGroup($userDN);
627
-		$gidGroupName = $this->getUserGroupByGid($userDN);
628
-
629
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
630
-
631
-		if (!empty($dynamicGroupMemberURL)) {
632
-			// look through dynamic groups to add them to the result array if needed
633
-			$groupsToMatch = $this->access->fetchListOfGroups(
634
-				$this->access->connection->ldapGroupFilter,array('dn',$dynamicGroupMemberURL));
635
-			foreach($groupsToMatch as $dynamicGroup) {
636
-				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
637
-					continue;
638
-				}
639
-				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
640
-				if ($pos !== false) {
641
-					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0],$pos);
642
-					// apply filter via ldap search to see if this user is in this
643
-					// dynamic group
644
-					$userMatch = $this->access->readAttribute(
645
-						$userDN,
646
-						$this->access->connection->ldapUserDisplayName,
647
-						$memberUrlFilter
648
-					);
649
-					if ($userMatch !== false) {
650
-						// match found so this user is in this group
651
-						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
652
-						if(is_string($groupName)) {
653
-							// be sure to never return false if the dn could not be
654
-							// resolved to a name, for whatever reason.
655
-							$groups[] = $groupName;
656
-						}
657
-					}
658
-				} else {
659
-					\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
660
-						'of group ' . print_r($dynamicGroup, true), ILogger::DEBUG);
661
-				}
662
-			}
663
-		}
664
-
665
-		// if possible, read out membership via memberOf. It's far faster than
666
-		// performing a search, which still is a fallback later.
667
-		// memberof doesn't support memberuid, so skip it here.
668
-		if((int)$this->access->connection->hasMemberOfFilterSupport === 1
669
-			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
670
-		    && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
671
-		    ) {
672
-			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
673
-			if (is_array($groupDNs)) {
674
-				foreach ($groupDNs as $dn) {
675
-					$groupName = $this->access->dn2groupname($dn);
676
-					if(is_string($groupName)) {
677
-						// be sure to never return false if the dn could not be
678
-						// resolved to a name, for whatever reason.
679
-						$groups[] = $groupName;
680
-					}
681
-				}
682
-			}
683
-
684
-			if($primaryGroup !== false) {
685
-				$groups[] = $primaryGroup;
686
-			}
687
-			if($gidGroupName !== false) {
688
-				$groups[] = $gidGroupName;
689
-			}
690
-			$this->access->connection->writeToCache($cacheKey, $groups);
691
-			return $groups;
692
-		}
693
-
694
-		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
695
-		if((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
696
-			|| (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
697
-		) {
698
-			$uid = $userDN;
699
-		} else if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
700
-			$result = $this->access->readAttribute($userDN, 'uid');
701
-			if ($result === false) {
702
-				\OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.
703
-					$this->access->connection->ldapHost, ILogger::DEBUG);
704
-			}
705
-			$uid = $result[0];
706
-		} else {
707
-			// just in case
708
-			$uid = $userDN;
709
-		}
710
-
711
-		if(isset($this->cachedGroupsByMember[$uid])) {
712
-			$groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
713
-		} else {
714
-			$groupsByMember = array_values($this->getGroupsByMember($uid));
715
-			$groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
716
-			$this->cachedGroupsByMember[$uid] = $groupsByMember;
717
-			$groups = array_merge($groups, $groupsByMember);
718
-		}
719
-
720
-		if($primaryGroup !== false) {
721
-			$groups[] = $primaryGroup;
722
-		}
723
-		if($gidGroupName !== false) {
724
-			$groups[] = $gidGroupName;
725
-		}
726
-
727
-		$groups = array_unique($groups, SORT_LOCALE_STRING);
728
-		$this->access->connection->writeToCache($cacheKey, $groups);
729
-
730
-		return $groups;
731
-	}
732
-
733
-	/**
734
-	 * @param string $dn
735
-	 * @param array|null &$seen
736
-	 * @return array
737
-	 */
738
-	private function getGroupsByMember($dn, &$seen = null) {
739
-		if ($seen === null) {
740
-			$seen = array();
741
-		}
742
-		$allGroups = array();
743
-		if (array_key_exists($dn, $seen)) {
744
-			// avoid loops
745
-			return array();
746
-		}
747
-		$seen[$dn] = true;
748
-		$filter = $this->access->combineFilterWithAnd(array(
749
-			$this->access->connection->ldapGroupFilter,
750
-			$this->access->connection->ldapGroupMemberAssocAttr.'='.$dn
751
-		));
752
-		$groups = $this->access->fetchListOfGroups($filter,
753
-			array($this->access->connection->ldapGroupDisplayName, 'dn'));
754
-		if (is_array($groups)) {
755
-			foreach ($groups as $groupobj) {
756
-				$groupDN = $groupobj['dn'][0];
757
-				$allGroups[$groupDN] = $groupobj;
758
-				$nestedGroups = $this->access->connection->ldapNestedGroups;
759
-				if (!empty($nestedGroups)) {
760
-					$supergroups = $this->getGroupsByMember($groupDN, $seen);
761
-					if (is_array($supergroups) && (count($supergroups)>0)) {
762
-						$allGroups = array_merge($allGroups, $supergroups);
763
-					}
764
-				}
765
-			}
766
-		}
767
-		return $allGroups;
768
-	}
769
-
770
-	/**
771
-	 * get a list of all users in a group
772
-	 *
773
-	 * @param string $gid
774
-	 * @param string $search
775
-	 * @param int $limit
776
-	 * @param int $offset
777
-	 * @return array with user ids
778
-	 */
779
-	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
780
-		if(!$this->enabled) {
781
-			return array();
782
-		}
783
-		if(!$this->groupExists($gid)) {
784
-			return array();
785
-		}
786
-		$search = $this->access->escapeFilterPart($search, true);
787
-		$cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
788
-		// check for cache of the exact query
789
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
790
-		if(!is_null($groupUsers)) {
791
-			return $groupUsers;
792
-		}
793
-
794
-		// check for cache of the query without limit and offset
795
-		$groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
796
-		if(!is_null($groupUsers)) {
797
-			$groupUsers = array_slice($groupUsers, $offset, $limit);
798
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
799
-			return $groupUsers;
800
-		}
801
-
802
-		if($limit === -1) {
803
-			$limit = null;
804
-		}
805
-		$groupDN = $this->access->groupname2dn($gid);
806
-		if(!$groupDN) {
807
-			// group couldn't be found, return empty resultset
808
-			$this->access->connection->writeToCache($cacheKey, array());
809
-			return array();
810
-		}
811
-
812
-		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
813
-		$posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
814
-		$members = array_keys($this->_groupMembers($groupDN));
815
-		if(!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
816
-			//in case users could not be retrieved, return empty result set
817
-			$this->access->connection->writeToCache($cacheKey, []);
818
-			return [];
819
-		}
820
-
821
-		$groupUsers = array();
822
-		$isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
823
-		$attrs = $this->access->userManager->getAttributes(true);
824
-		foreach($members as $member) {
825
-			if($isMemberUid) {
826
-				//we got uids, need to get their DNs to 'translate' them to user names
827
-				$filter = $this->access->combineFilterWithAnd(array(
828
-					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
829
-					$this->access->getFilterPartForUserSearch($search)
830
-				));
831
-				$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
832
-				if(count($ldap_users) < 1) {
833
-					continue;
834
-				}
835
-				$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
836
-			} else {
837
-				//we got DNs, check if we need to filter by search or we can give back all of them
838
-				if ($search !== '') {
839
-					if(!$this->access->readAttribute($member,
840
-						$this->access->connection->ldapUserDisplayName,
841
-						$this->access->getFilterPartForUserSearch($search))) {
842
-						continue;
843
-					}
844
-				}
845
-				// dn2username will also check if the users belong to the allowed base
846
-				if($ocname = $this->access->dn2username($member)) {
847
-					$groupUsers[] = $ocname;
848
-				}
849
-			}
850
-		}
851
-
852
-		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
853
-		natsort($groupUsers);
854
-		$this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
855
-		$groupUsers = array_slice($groupUsers, $offset, $limit);
856
-
857
-		$this->access->connection->writeToCache($cacheKey, $groupUsers);
858
-
859
-		return $groupUsers;
860
-	}
861
-
862
-	/**
863
-	 * returns the number of users in a group, who match the search term
864
-	 * @param string $gid the internal group name
865
-	 * @param string $search optional, a search string
866
-	 * @return int|bool
867
-	 */
868
-	public function countUsersInGroup($gid, $search = '') {
869
-		if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
870
-			return $this->groupPluginManager->countUsersInGroup($gid, $search);
871
-		}
872
-
873
-		$cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
874
-		if(!$this->enabled || !$this->groupExists($gid)) {
875
-			return false;
876
-		}
877
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
878
-		if(!is_null($groupUsers)) {
879
-			return $groupUsers;
880
-		}
881
-
882
-		$groupDN = $this->access->groupname2dn($gid);
883
-		if(!$groupDN) {
884
-			// group couldn't be found, return empty result set
885
-			$this->access->connection->writeToCache($cacheKey, false);
886
-			return false;
887
-		}
888
-
889
-		$members = array_keys($this->_groupMembers($groupDN));
890
-		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
891
-		if(!$members && $primaryUserCount === 0) {
892
-			//in case users could not be retrieved, return empty result set
893
-			$this->access->connection->writeToCache($cacheKey, false);
894
-			return false;
895
-		}
896
-
897
-		if ($search === '') {
898
-			$groupUsers = count($members) + $primaryUserCount;
899
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
900
-			return $groupUsers;
901
-		}
902
-		$search = $this->access->escapeFilterPart($search, true);
903
-		$isMemberUid =
904
-			(strtolower($this->access->connection->ldapGroupMemberAssocAttr)
905
-			=== 'memberuid');
906
-
907
-		//we need to apply the search filter
908
-		//alternatives that need to be checked:
909
-		//a) get all users by search filter and array_intersect them
910
-		//b) a, but only when less than 1k 10k ?k users like it is
911
-		//c) put all DNs|uids in a LDAP filter, combine with the search string
912
-		//   and let it count.
913
-		//For now this is not important, because the only use of this method
914
-		//does not supply a search string
915
-		$groupUsers = array();
916
-		foreach($members as $member) {
917
-			if($isMemberUid) {
918
-				//we got uids, need to get their DNs to 'translate' them to user names
919
-				$filter = $this->access->combineFilterWithAnd(array(
920
-					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
921
-					$this->access->getFilterPartForUserSearch($search)
922
-				));
923
-				$ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
924
-				if(count($ldap_users) < 1) {
925
-					continue;
926
-				}
927
-				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
928
-			} else {
929
-				//we need to apply the search filter now
930
-				if(!$this->access->readAttribute($member,
931
-					$this->access->connection->ldapUserDisplayName,
932
-					$this->access->getFilterPartForUserSearch($search))) {
933
-					continue;
934
-				}
935
-				// dn2username will also check if the users belong to the allowed base
936
-				if($ocname = $this->access->dn2username($member)) {
937
-					$groupUsers[] = $ocname;
938
-				}
939
-			}
940
-		}
941
-
942
-		//and get users that have the group as primary
943
-		$primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
944
-
945
-		return count($groupUsers) + $primaryUsers;
946
-	}
947
-
948
-	/**
949
-	 * get a list of all groups
950
-	 *
951
-	 * @param string $search
952
-	 * @param $limit
953
-	 * @param int $offset
954
-	 * @return array with group names
955
-	 *
956
-	 * Returns a list with all groups (used by getGroups)
957
-	 */
958
-	protected function getGroupsChunk($search = '', $limit = -1, $offset = 0) {
959
-		if(!$this->enabled) {
960
-			return array();
961
-		}
962
-		$cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
963
-
964
-		//Check cache before driving unnecessary searches
965
-		\OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, ILogger::DEBUG);
966
-		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
967
-		if(!is_null($ldap_groups)) {
968
-			return $ldap_groups;
969
-		}
970
-
971
-		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
972
-		// error. With a limit of 0, we get 0 results. So we pass null.
973
-		if($limit <= 0) {
974
-			$limit = null;
975
-		}
976
-		$filter = $this->access->combineFilterWithAnd(array(
977
-			$this->access->connection->ldapGroupFilter,
978
-			$this->access->getFilterPartForGroupSearch($search)
979
-		));
980
-		\OCP\Util::writeLog('user_ldap', 'getGroups Filter '.$filter, ILogger::DEBUG);
981
-		$ldap_groups = $this->access->fetchListOfGroups($filter,
982
-				array($this->access->connection->ldapGroupDisplayName, 'dn'),
983
-				$limit,
984
-				$offset);
985
-		$ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
986
-
987
-		$this->access->connection->writeToCache($cacheKey, $ldap_groups);
988
-		return $ldap_groups;
989
-	}
990
-
991
-	/**
992
-	 * get a list of all groups using a paged search
993
-	 *
994
-	 * @param string $search
995
-	 * @param int $limit
996
-	 * @param int $offset
997
-	 * @return array with group names
998
-	 *
999
-	 * Returns a list with all groups
1000
-	 * Uses a paged search if available to override a
1001
-	 * server side search limit.
1002
-	 * (active directory has a limit of 1000 by default)
1003
-	 */
1004
-	public function getGroups($search = '', $limit = -1, $offset = 0) {
1005
-		if(!$this->enabled) {
1006
-			return array();
1007
-		}
1008
-		$search = $this->access->escapeFilterPart($search, true);
1009
-		$pagingSize = (int)$this->access->connection->ldapPagingSize;
1010
-		if (!$this->access->connection->hasPagedResultSupport || $pagingSize <= 0) {
1011
-			return $this->getGroupsChunk($search, $limit, $offset);
1012
-		}
1013
-		$maxGroups = 100000; // limit max results (just for safety reasons)
1014
-		if ($limit > -1) {
1015
-		   $overallLimit = min($limit + $offset, $maxGroups);
1016
-		} else {
1017
-		   $overallLimit = $maxGroups;
1018
-		}
1019
-		$chunkOffset = $offset;
1020
-		$allGroups = array();
1021
-		while ($chunkOffset < $overallLimit) {
1022
-			$chunkLimit = min($pagingSize, $overallLimit - $chunkOffset);
1023
-			$ldapGroups = $this->getGroupsChunk($search, $chunkLimit, $chunkOffset);
1024
-			$nread = count($ldapGroups);
1025
-			\OCP\Util::writeLog('user_ldap', 'getGroups('.$search.'): read '.$nread.' at offset '.$chunkOffset.' (limit: '.$chunkLimit.')', ILogger::DEBUG);
1026
-			if ($nread) {
1027
-				$allGroups = array_merge($allGroups, $ldapGroups);
1028
-				$chunkOffset += $nread;
1029
-			}
1030
-			if ($nread < $chunkLimit) {
1031
-				break;
1032
-			}
1033
-		}
1034
-		return $allGroups;
1035
-	}
1036
-
1037
-	/**
1038
-	 * @param string $group
1039
-	 * @return bool
1040
-	 */
1041
-	public function groupMatchesFilter($group) {
1042
-		return (strripos($group, $this->groupSearch) !== false);
1043
-	}
1044
-
1045
-	/**
1046
-	 * check if a group exists
1047
-	 * @param string $gid
1048
-	 * @return bool
1049
-	 */
1050
-	public function groupExists($gid) {
1051
-		$groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1052
-		if(!is_null($groupExists)) {
1053
-			return (bool)$groupExists;
1054
-		}
1055
-
1056
-		//getting dn, if false the group does not exist. If dn, it may be mapped
1057
-		//only, requires more checking.
1058
-		$dn = $this->access->groupname2dn($gid);
1059
-		if(!$dn) {
1060
-			$this->access->connection->writeToCache('groupExists'.$gid, false);
1061
-			return false;
1062
-		}
1063
-
1064
-		//if group really still exists, we will be able to read its objectclass
1065
-		if(!is_array($this->access->readAttribute($dn, ''))) {
1066
-			$this->access->connection->writeToCache('groupExists'.$gid, false);
1067
-			return false;
1068
-		}
1069
-
1070
-		$this->access->connection->writeToCache('groupExists'.$gid, true);
1071
-		return true;
1072
-	}
1073
-
1074
-	/**
1075
-	* Check if backend implements actions
1076
-	* @param int $actions bitwise-or'ed actions
1077
-	* @return boolean
1078
-	*
1079
-	* Returns the supported actions as int to be
1080
-	* compared with GroupInterface::CREATE_GROUP etc.
1081
-	*/
1082
-	public function implementsActions($actions) {
1083
-		return (bool)((GroupInterface::COUNT_USERS |
1084
-				$this->groupPluginManager->getImplementedActions()) & $actions);
1085
-	}
1086
-
1087
-	/**
1088
-	 * Return access for LDAP interaction.
1089
-	 * @return Access instance of Access for LDAP interaction
1090
-	 */
1091
-	public function getLDAPAccess($gid) {
1092
-		return $this->access;
1093
-	}
1094
-
1095
-	/**
1096
-	 * create a group
1097
-	 * @param string $gid
1098
-	 * @return bool
1099
-	 * @throws \Exception
1100
-	 */
1101
-	public function createGroup($gid) {
1102
-		if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1103
-			if ($dn = $this->groupPluginManager->createGroup($gid)) {
1104
-				//updates group mapping
1105
-				$this->access->dn2ocname($dn, $gid, false);
1106
-				$this->access->connection->writeToCache("groupExists".$gid, true);
1107
-			}
1108
-			return $dn != null;
1109
-		}
1110
-		throw new \Exception('Could not create group in LDAP backend.');
1111
-	}
1112
-
1113
-	/**
1114
-	 * delete a group
1115
-	 * @param string $gid gid of the group to delete
1116
-	 * @return bool
1117
-	 * @throws \Exception
1118
-	 */
1119
-	public function deleteGroup($gid) {
1120
-		if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1121
-			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1122
-				#delete group in nextcloud internal db
1123
-				$this->access->getGroupMapper()->unmap($gid);
1124
-				$this->access->connection->writeToCache("groupExists".$gid, false);
1125
-			}
1126
-			return $ret;
1127
-		}
1128
-		throw new \Exception('Could not delete group in LDAP backend.');
1129
-	}
1130
-
1131
-	/**
1132
-	 * Add a user to a group
1133
-	 * @param string $uid Name of the user to add to group
1134
-	 * @param string $gid Name of the group in which add the user
1135
-	 * @return bool
1136
-	 * @throws \Exception
1137
-	 */
1138
-	public function addToGroup($uid, $gid) {
1139
-		if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1140
-			if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1141
-				$this->access->connection->clearCache();
1142
-			}
1143
-			return $ret;
1144
-		}
1145
-		throw new \Exception('Could not add user to group in LDAP backend.');
1146
-	}
1147
-
1148
-	/**
1149
-	 * Removes a user from a group
1150
-	 * @param string $uid Name of the user to remove from group
1151
-	 * @param string $gid Name of the group from which remove the user
1152
-	 * @return bool
1153
-	 * @throws \Exception
1154
-	 */
1155
-	public function removeFromGroup($uid, $gid) {
1156
-		if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1157
-			if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1158
-				$this->access->connection->clearCache();
1159
-			}
1160
-			return $ret;
1161
-		}
1162
-		throw new \Exception('Could not remove user from group in LDAP backend.');
1163
-	}
1164
-
1165
-	/**
1166
-	 * Gets group details
1167
-	 * @param string $gid Name of the group
1168
-	 * @return array | false
1169
-	 * @throws \Exception
1170
-	 */
1171
-	public function getGroupDetails($gid) {
1172
-		if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1173
-			return $this->groupPluginManager->getGroupDetails($gid);
1174
-		}
1175
-		throw new \Exception('Could not get group details in LDAP backend.');
1176
-	}
1177
-
1178
-	/**
1179
-	 * Return LDAP connection resource from a cloned connection.
1180
-	 * The cloned connection needs to be closed manually.
1181
-	 * of the current access.
1182
-	 * @param string $gid
1183
-	 * @return resource of the LDAP connection
1184
-	 */
1185
-	public function getNewLDAPConnection($gid) {
1186
-		$connection = clone $this->access->getConnection();
1187
-		return $connection->getConnectionResource();
1188
-	}
49
+    protected $enabled = false;
50
+
51
+    /**
52
+     * @var string[] $cachedGroupMembers array of users with gid as key
53
+     */
54
+    protected $cachedGroupMembers;
55
+
56
+    /**
57
+     * @var string[] $cachedGroupsByMember array of groups with uid as key
58
+     */
59
+    protected $cachedGroupsByMember;
60
+
61
+    /** @var GroupPluginManager */
62
+    protected $groupPluginManager;
63
+
64
+    public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
65
+        parent::__construct($access);
66
+        $filter = $this->access->connection->ldapGroupFilter;
67
+        $gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
68
+        if(!empty($filter) && !empty($gassoc)) {
69
+            $this->enabled = true;
70
+        }
71
+
72
+        $this->cachedGroupMembers = new CappedMemoryCache();
73
+        $this->cachedGroupsByMember = new CappedMemoryCache();
74
+        $this->groupPluginManager = $groupPluginManager;
75
+    }
76
+
77
+    /**
78
+     * is user in group?
79
+     * @param string $uid uid of the user
80
+     * @param string $gid gid of the group
81
+     * @return bool
82
+     *
83
+     * Checks whether the user is member of a group or not.
84
+     */
85
+    public function inGroup($uid, $gid) {
86
+        if(!$this->enabled) {
87
+            return false;
88
+        }
89
+        $cacheKey = 'inGroup'.$uid.':'.$gid;
90
+        $inGroup = $this->access->connection->getFromCache($cacheKey);
91
+        if(!is_null($inGroup)) {
92
+            return (bool)$inGroup;
93
+        }
94
+
95
+        $userDN = $this->access->username2dn($uid);
96
+
97
+        if(isset($this->cachedGroupMembers[$gid])) {
98
+            $isInGroup = in_array($userDN, $this->cachedGroupMembers[$gid]);
99
+            return $isInGroup;
100
+        }
101
+
102
+        $cacheKeyMembers = 'inGroup-members:'.$gid;
103
+        $members = $this->access->connection->getFromCache($cacheKeyMembers);
104
+        if(!is_null($members)) {
105
+            $this->cachedGroupMembers[$gid] = $members;
106
+            $isInGroup = in_array($userDN, $members);
107
+            $this->access->connection->writeToCache($cacheKey, $isInGroup);
108
+            return $isInGroup;
109
+        }
110
+
111
+        $groupDN = $this->access->groupname2dn($gid);
112
+        // just in case
113
+        if(!$groupDN || !$userDN) {
114
+            $this->access->connection->writeToCache($cacheKey, false);
115
+            return false;
116
+        }
117
+
118
+        //check primary group first
119
+        if($gid === $this->getUserPrimaryGroup($userDN)) {
120
+            $this->access->connection->writeToCache($cacheKey, true);
121
+            return true;
122
+        }
123
+
124
+        //usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
125
+        $members = $this->_groupMembers($groupDN);
126
+        $members = array_keys($members); // uids are returned as keys
127
+        if(!is_array($members) || count($members) === 0) {
128
+            $this->access->connection->writeToCache($cacheKey, false);
129
+            return false;
130
+        }
131
+
132
+        //extra work if we don't get back user DNs
133
+        if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
134
+            $dns = array();
135
+            $filterParts = array();
136
+            $bytes = 0;
137
+            foreach($members as $mid) {
138
+                $filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
139
+                $filterParts[] = $filter;
140
+                $bytes += strlen($filter);
141
+                if($bytes >= 9000000) {
142
+                    // AD has a default input buffer of 10 MB, we do not want
143
+                    // to take even the chance to exceed it
144
+                    $filter = $this->access->combineFilterWithOr($filterParts);
145
+                    $bytes = 0;
146
+                    $filterParts = array();
147
+                    $users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
148
+                    $dns = array_merge($dns, $users);
149
+                }
150
+            }
151
+            if(count($filterParts) > 0) {
152
+                $filter = $this->access->combineFilterWithOr($filterParts);
153
+                $users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
154
+                $dns = array_merge($dns, $users);
155
+            }
156
+            $members = $dns;
157
+        }
158
+
159
+        $isInGroup = in_array($userDN, $members);
160
+        $this->access->connection->writeToCache($cacheKey, $isInGroup);
161
+        $this->access->connection->writeToCache($cacheKeyMembers, $members);
162
+        $this->cachedGroupMembers[$gid] = $members;
163
+
164
+        return $isInGroup;
165
+    }
166
+
167
+    /**
168
+     * @param string $dnGroup
169
+     * @return array
170
+     *
171
+     * For a group that has user membership defined by an LDAP search url attribute returns the users
172
+     * that match the search url otherwise returns an empty array.
173
+     */
174
+    public function getDynamicGroupMembers($dnGroup) {
175
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
176
+
177
+        if (empty($dynamicGroupMemberURL)) {
178
+            return array();
179
+        }
180
+
181
+        $dynamicMembers = array();
182
+        $memberURLs = $this->access->readAttribute(
183
+            $dnGroup,
184
+            $dynamicGroupMemberURL,
185
+            $this->access->connection->ldapGroupFilter
186
+        );
187
+        if ($memberURLs !== false) {
188
+            // this group has the 'memberURL' attribute so this is a dynamic group
189
+            // example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
190
+            // example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
191
+            $pos = strpos($memberURLs[0], '(');
192
+            if ($pos !== false) {
193
+                $memberUrlFilter = substr($memberURLs[0], $pos);
194
+                $foundMembers = $this->access->searchUsers($memberUrlFilter,'dn');
195
+                $dynamicMembers = array();
196
+                foreach($foundMembers as $value) {
197
+                    $dynamicMembers[$value['dn'][0]] = 1;
198
+                }
199
+            } else {
200
+                \OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
201
+                    'of group ' . $dnGroup, ILogger::DEBUG);
202
+            }
203
+        }
204
+        return $dynamicMembers;
205
+    }
206
+
207
+    /**
208
+     * @param string $dnGroup
209
+     * @param array|null &$seen
210
+     * @return array|mixed|null
211
+     * @throws \OC\ServerNotAvailableException
212
+     */
213
+    private function _groupMembers($dnGroup, &$seen = null) {
214
+        if ($seen === null) {
215
+            $seen = array();
216
+        }
217
+        $allMembers = array();
218
+        if (array_key_exists($dnGroup, $seen)) {
219
+            // avoid loops
220
+            return array();
221
+        }
222
+        // used extensively in cron job, caching makes sense for nested groups
223
+        $cacheKey = '_groupMembers'.$dnGroup;
224
+        $groupMembers = $this->access->connection->getFromCache($cacheKey);
225
+        if($groupMembers !== null) {
226
+            return $groupMembers;
227
+        }
228
+        $seen[$dnGroup] = 1;
229
+        $members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr,
230
+                                                $this->access->connection->ldapGroupFilter);
231
+        if (is_array($members)) {
232
+            foreach ($members as $member) {
233
+                $allMembers[$member] = 1;
234
+                $nestedGroups = $this->access->connection->ldapNestedGroups;
235
+                if (!empty($nestedGroups)) {
236
+                    $subMembers = $this->_groupMembers($member, $seen);
237
+                    if ($subMembers) {
238
+                        $allMembers += $subMembers;
239
+                    }
240
+                }
241
+            }
242
+        }
243
+
244
+        $allMembers += $this->getDynamicGroupMembers($dnGroup);
245
+
246
+        $this->access->connection->writeToCache($cacheKey, $allMembers);
247
+        return $allMembers;
248
+    }
249
+
250
+    /**
251
+     * @param string $DN
252
+     * @param array|null &$seen
253
+     * @return array
254
+     */
255
+    private function _getGroupDNsFromMemberOf($DN, &$seen = null) {
256
+        if ($seen === null) {
257
+            $seen = array();
258
+        }
259
+        if (array_key_exists($DN, $seen)) {
260
+            // avoid loops
261
+            return array();
262
+        }
263
+        $seen[$DN] = 1;
264
+        $groups = $this->access->readAttribute($DN, 'memberOf');
265
+        if (!is_array($groups)) {
266
+            return array();
267
+        }
268
+        $groups = $this->access->groupsMatchFilter($groups);
269
+        $allGroups =  $groups;
270
+        $nestedGroups = $this->access->connection->ldapNestedGroups;
271
+        if ((int)$nestedGroups === 1) {
272
+            foreach ($groups as $group) {
273
+                $subGroups = $this->_getGroupDNsFromMemberOf($group, $seen);
274
+                $allGroups = array_merge($allGroups, $subGroups);
275
+            }
276
+        }
277
+        return $allGroups;
278
+    }
279
+
280
+    /**
281
+     * translates a gidNumber into an ownCloud internal name
282
+     * @param string $gid as given by gidNumber on POSIX LDAP
283
+     * @param string $dn a DN that belongs to the same domain as the group
284
+     * @return string|bool
285
+     */
286
+    public function gidNumber2Name($gid, $dn) {
287
+        $cacheKey = 'gidNumberToName' . $gid;
288
+        $groupName = $this->access->connection->getFromCache($cacheKey);
289
+        if(!is_null($groupName) && isset($groupName)) {
290
+            return $groupName;
291
+        }
292
+
293
+        //we need to get the DN from LDAP
294
+        $filter = $this->access->combineFilterWithAnd([
295
+            $this->access->connection->ldapGroupFilter,
296
+            'objectClass=posixGroup',
297
+            $this->access->connection->ldapGidNumber . '=' . $gid
298
+        ]);
299
+        $result = $this->access->searchGroups($filter, array('dn'), 1);
300
+        if(empty($result)) {
301
+            return false;
302
+        }
303
+        $dn = $result[0]['dn'][0];
304
+
305
+        //and now the group name
306
+        //NOTE once we have separate ownCloud group IDs and group names we can
307
+        //directly read the display name attribute instead of the DN
308
+        $name = $this->access->dn2groupname($dn);
309
+
310
+        $this->access->connection->writeToCache($cacheKey, $name);
311
+
312
+        return $name;
313
+    }
314
+
315
+    /**
316
+     * returns the entry's gidNumber
317
+     * @param string $dn
318
+     * @param string $attribute
319
+     * @return string|bool
320
+     */
321
+    private function getEntryGidNumber($dn, $attribute) {
322
+        $value = $this->access->readAttribute($dn, $attribute);
323
+        if(is_array($value) && !empty($value)) {
324
+            return $value[0];
325
+        }
326
+        return false;
327
+    }
328
+
329
+    /**
330
+     * returns the group's primary ID
331
+     * @param string $dn
332
+     * @return string|bool
333
+     */
334
+    public function getGroupGidNumber($dn) {
335
+        return $this->getEntryGidNumber($dn, 'gidNumber');
336
+    }
337
+
338
+    /**
339
+     * returns the user's gidNumber
340
+     * @param string $dn
341
+     * @return string|bool
342
+     */
343
+    public function getUserGidNumber($dn) {
344
+        $gidNumber = false;
345
+        if($this->access->connection->hasGidNumber) {
346
+            $gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
347
+            if($gidNumber === false) {
348
+                $this->access->connection->hasGidNumber = false;
349
+            }
350
+        }
351
+        return $gidNumber;
352
+    }
353
+
354
+    /**
355
+     * returns a filter for a "users has specific gid" search or count operation
356
+     *
357
+     * @param string $groupDN
358
+     * @param string $search
359
+     * @return string
360
+     * @throws \Exception
361
+     */
362
+    private function prepareFilterForUsersHasGidNumber($groupDN, $search = '') {
363
+        $groupID = $this->getGroupGidNumber($groupDN);
364
+        if($groupID === false) {
365
+            throw new \Exception('Not a valid group');
366
+        }
367
+
368
+        $filterParts = [];
369
+        $filterParts[] = $this->access->getFilterForUserCount();
370
+        if ($search !== '') {
371
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
372
+        }
373
+        $filterParts[] = $this->access->connection->ldapGidNumber .'=' . $groupID;
374
+
375
+        return $this->access->combineFilterWithAnd($filterParts);
376
+    }
377
+
378
+    /**
379
+     * returns a list of users that have the given group as gid number
380
+     *
381
+     * @param string $groupDN
382
+     * @param string $search
383
+     * @param int $limit
384
+     * @param int $offset
385
+     * @return string[]
386
+     */
387
+    public function getUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
388
+        try {
389
+            $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
390
+            $users = $this->access->fetchListOfUsers(
391
+                $filter,
392
+                [$this->access->connection->ldapUserDisplayName, 'dn'],
393
+                $limit,
394
+                $offset
395
+            );
396
+            return $this->access->nextcloudUserNames($users);
397
+        } catch (\Exception $e) {
398
+            return [];
399
+        }
400
+    }
401
+
402
+    /**
403
+     * returns the number of users that have the given group as gid number
404
+     *
405
+     * @param string $groupDN
406
+     * @param string $search
407
+     * @param int $limit
408
+     * @param int $offset
409
+     * @return int
410
+     */
411
+    public function countUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
412
+        try {
413
+            $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
414
+            $users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
415
+            return (int)$users;
416
+        } catch (\Exception $e) {
417
+            return 0;
418
+        }
419
+    }
420
+
421
+    /**
422
+     * gets the gidNumber of a user
423
+     * @param string $dn
424
+     * @return string
425
+     */
426
+    public function getUserGroupByGid($dn) {
427
+        $groupID = $this->getUserGidNumber($dn);
428
+        if($groupID !== false) {
429
+            $groupName = $this->gidNumber2Name($groupID, $dn);
430
+            if($groupName !== false) {
431
+                return $groupName;
432
+            }
433
+        }
434
+
435
+        return false;
436
+    }
437
+
438
+    /**
439
+     * translates a primary group ID into an Nextcloud internal name
440
+     * @param string $gid as given by primaryGroupID on AD
441
+     * @param string $dn a DN that belongs to the same domain as the group
442
+     * @return string|bool
443
+     */
444
+    public function primaryGroupID2Name($gid, $dn) {
445
+        $cacheKey = 'primaryGroupIDtoName';
446
+        $groupNames = $this->access->connection->getFromCache($cacheKey);
447
+        if(!is_null($groupNames) && isset($groupNames[$gid])) {
448
+            return $groupNames[$gid];
449
+        }
450
+
451
+        $domainObjectSid = $this->access->getSID($dn);
452
+        if($domainObjectSid === false) {
453
+            return false;
454
+        }
455
+
456
+        //we need to get the DN from LDAP
457
+        $filter = $this->access->combineFilterWithAnd(array(
458
+            $this->access->connection->ldapGroupFilter,
459
+            'objectsid=' . $domainObjectSid . '-' . $gid
460
+        ));
461
+        $result = $this->access->searchGroups($filter, array('dn'), 1);
462
+        if(empty($result)) {
463
+            return false;
464
+        }
465
+        $dn = $result[0]['dn'][0];
466
+
467
+        //and now the group name
468
+        //NOTE once we have separate Nextcloud group IDs and group names we can
469
+        //directly read the display name attribute instead of the DN
470
+        $name = $this->access->dn2groupname($dn);
471
+
472
+        $this->access->connection->writeToCache($cacheKey, $name);
473
+
474
+        return $name;
475
+    }
476
+
477
+    /**
478
+     * returns the entry's primary group ID
479
+     * @param string $dn
480
+     * @param string $attribute
481
+     * @return string|bool
482
+     */
483
+    private function getEntryGroupID($dn, $attribute) {
484
+        $value = $this->access->readAttribute($dn, $attribute);
485
+        if(is_array($value) && !empty($value)) {
486
+            return $value[0];
487
+        }
488
+        return false;
489
+    }
490
+
491
+    /**
492
+     * returns the group's primary ID
493
+     * @param string $dn
494
+     * @return string|bool
495
+     */
496
+    public function getGroupPrimaryGroupID($dn) {
497
+        return $this->getEntryGroupID($dn, 'primaryGroupToken');
498
+    }
499
+
500
+    /**
501
+     * returns the user's primary group ID
502
+     * @param string $dn
503
+     * @return string|bool
504
+     */
505
+    public function getUserPrimaryGroupIDs($dn) {
506
+        $primaryGroupID = false;
507
+        if($this->access->connection->hasPrimaryGroups) {
508
+            $primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
509
+            if($primaryGroupID === false) {
510
+                $this->access->connection->hasPrimaryGroups = false;
511
+            }
512
+        }
513
+        return $primaryGroupID;
514
+    }
515
+
516
+    /**
517
+     * returns a filter for a "users in primary group" search or count operation
518
+     *
519
+     * @param string $groupDN
520
+     * @param string $search
521
+     * @return string
522
+     * @throws \Exception
523
+     */
524
+    private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
525
+        $groupID = $this->getGroupPrimaryGroupID($groupDN);
526
+        if($groupID === false) {
527
+            throw new \Exception('Not a valid group');
528
+        }
529
+
530
+        $filterParts = [];
531
+        $filterParts[] = $this->access->getFilterForUserCount();
532
+        if ($search !== '') {
533
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
534
+        }
535
+        $filterParts[] = 'primaryGroupID=' . $groupID;
536
+
537
+        return $this->access->combineFilterWithAnd($filterParts);
538
+    }
539
+
540
+    /**
541
+     * returns a list of users that have the given group as primary group
542
+     *
543
+     * @param string $groupDN
544
+     * @param string $search
545
+     * @param int $limit
546
+     * @param int $offset
547
+     * @return string[]
548
+     */
549
+    public function getUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
550
+        try {
551
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
552
+            $users = $this->access->fetchListOfUsers(
553
+                $filter,
554
+                array($this->access->connection->ldapUserDisplayName, 'dn'),
555
+                $limit,
556
+                $offset
557
+            );
558
+            return $this->access->nextcloudUserNames($users);
559
+        } catch (\Exception $e) {
560
+            return array();
561
+        }
562
+    }
563
+
564
+    /**
565
+     * returns the number of users that have the given group as primary group
566
+     *
567
+     * @param string $groupDN
568
+     * @param string $search
569
+     * @param int $limit
570
+     * @param int $offset
571
+     * @return int
572
+     */
573
+    public function countUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
574
+        try {
575
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
576
+            $users = $this->access->countUsers($filter, array('dn'), $limit, $offset);
577
+            return (int)$users;
578
+        } catch (\Exception $e) {
579
+            return 0;
580
+        }
581
+    }
582
+
583
+    /**
584
+     * gets the primary group of a user
585
+     * @param string $dn
586
+     * @return string
587
+     */
588
+    public function getUserPrimaryGroup($dn) {
589
+        $groupID = $this->getUserPrimaryGroupIDs($dn);
590
+        if($groupID !== false) {
591
+            $groupName = $this->primaryGroupID2Name($groupID, $dn);
592
+            if($groupName !== false) {
593
+                return $groupName;
594
+            }
595
+        }
596
+
597
+        return false;
598
+    }
599
+
600
+    /**
601
+     * Get all groups a user belongs to
602
+     * @param string $uid Name of the user
603
+     * @return array with group names
604
+     *
605
+     * This function fetches all groups a user belongs to. It does not check
606
+     * if the user exists at all.
607
+     *
608
+     * This function includes groups based on dynamic group membership.
609
+     */
610
+    public function getUserGroups($uid) {
611
+        if(!$this->enabled) {
612
+            return array();
613
+        }
614
+        $cacheKey = 'getUserGroups'.$uid;
615
+        $userGroups = $this->access->connection->getFromCache($cacheKey);
616
+        if(!is_null($userGroups)) {
617
+            return $userGroups;
618
+        }
619
+        $userDN = $this->access->username2dn($uid);
620
+        if(!$userDN) {
621
+            $this->access->connection->writeToCache($cacheKey, array());
622
+            return array();
623
+        }
624
+
625
+        $groups = [];
626
+        $primaryGroup = $this->getUserPrimaryGroup($userDN);
627
+        $gidGroupName = $this->getUserGroupByGid($userDN);
628
+
629
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
630
+
631
+        if (!empty($dynamicGroupMemberURL)) {
632
+            // look through dynamic groups to add them to the result array if needed
633
+            $groupsToMatch = $this->access->fetchListOfGroups(
634
+                $this->access->connection->ldapGroupFilter,array('dn',$dynamicGroupMemberURL));
635
+            foreach($groupsToMatch as $dynamicGroup) {
636
+                if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
637
+                    continue;
638
+                }
639
+                $pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
640
+                if ($pos !== false) {
641
+                    $memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0],$pos);
642
+                    // apply filter via ldap search to see if this user is in this
643
+                    // dynamic group
644
+                    $userMatch = $this->access->readAttribute(
645
+                        $userDN,
646
+                        $this->access->connection->ldapUserDisplayName,
647
+                        $memberUrlFilter
648
+                    );
649
+                    if ($userMatch !== false) {
650
+                        // match found so this user is in this group
651
+                        $groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
652
+                        if(is_string($groupName)) {
653
+                            // be sure to never return false if the dn could not be
654
+                            // resolved to a name, for whatever reason.
655
+                            $groups[] = $groupName;
656
+                        }
657
+                    }
658
+                } else {
659
+                    \OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
660
+                        'of group ' . print_r($dynamicGroup, true), ILogger::DEBUG);
661
+                }
662
+            }
663
+        }
664
+
665
+        // if possible, read out membership via memberOf. It's far faster than
666
+        // performing a search, which still is a fallback later.
667
+        // memberof doesn't support memberuid, so skip it here.
668
+        if((int)$this->access->connection->hasMemberOfFilterSupport === 1
669
+            && (int)$this->access->connection->useMemberOfToDetectMembership === 1
670
+            && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
671
+            ) {
672
+            $groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
673
+            if (is_array($groupDNs)) {
674
+                foreach ($groupDNs as $dn) {
675
+                    $groupName = $this->access->dn2groupname($dn);
676
+                    if(is_string($groupName)) {
677
+                        // be sure to never return false if the dn could not be
678
+                        // resolved to a name, for whatever reason.
679
+                        $groups[] = $groupName;
680
+                    }
681
+                }
682
+            }
683
+
684
+            if($primaryGroup !== false) {
685
+                $groups[] = $primaryGroup;
686
+            }
687
+            if($gidGroupName !== false) {
688
+                $groups[] = $gidGroupName;
689
+            }
690
+            $this->access->connection->writeToCache($cacheKey, $groups);
691
+            return $groups;
692
+        }
693
+
694
+        //uniqueMember takes DN, memberuid the uid, so we need to distinguish
695
+        if((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
696
+            || (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
697
+        ) {
698
+            $uid = $userDN;
699
+        } else if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
700
+            $result = $this->access->readAttribute($userDN, 'uid');
701
+            if ($result === false) {
702
+                \OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.
703
+                    $this->access->connection->ldapHost, ILogger::DEBUG);
704
+            }
705
+            $uid = $result[0];
706
+        } else {
707
+            // just in case
708
+            $uid = $userDN;
709
+        }
710
+
711
+        if(isset($this->cachedGroupsByMember[$uid])) {
712
+            $groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
713
+        } else {
714
+            $groupsByMember = array_values($this->getGroupsByMember($uid));
715
+            $groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
716
+            $this->cachedGroupsByMember[$uid] = $groupsByMember;
717
+            $groups = array_merge($groups, $groupsByMember);
718
+        }
719
+
720
+        if($primaryGroup !== false) {
721
+            $groups[] = $primaryGroup;
722
+        }
723
+        if($gidGroupName !== false) {
724
+            $groups[] = $gidGroupName;
725
+        }
726
+
727
+        $groups = array_unique($groups, SORT_LOCALE_STRING);
728
+        $this->access->connection->writeToCache($cacheKey, $groups);
729
+
730
+        return $groups;
731
+    }
732
+
733
+    /**
734
+     * @param string $dn
735
+     * @param array|null &$seen
736
+     * @return array
737
+     */
738
+    private function getGroupsByMember($dn, &$seen = null) {
739
+        if ($seen === null) {
740
+            $seen = array();
741
+        }
742
+        $allGroups = array();
743
+        if (array_key_exists($dn, $seen)) {
744
+            // avoid loops
745
+            return array();
746
+        }
747
+        $seen[$dn] = true;
748
+        $filter = $this->access->combineFilterWithAnd(array(
749
+            $this->access->connection->ldapGroupFilter,
750
+            $this->access->connection->ldapGroupMemberAssocAttr.'='.$dn
751
+        ));
752
+        $groups = $this->access->fetchListOfGroups($filter,
753
+            array($this->access->connection->ldapGroupDisplayName, 'dn'));
754
+        if (is_array($groups)) {
755
+            foreach ($groups as $groupobj) {
756
+                $groupDN = $groupobj['dn'][0];
757
+                $allGroups[$groupDN] = $groupobj;
758
+                $nestedGroups = $this->access->connection->ldapNestedGroups;
759
+                if (!empty($nestedGroups)) {
760
+                    $supergroups = $this->getGroupsByMember($groupDN, $seen);
761
+                    if (is_array($supergroups) && (count($supergroups)>0)) {
762
+                        $allGroups = array_merge($allGroups, $supergroups);
763
+                    }
764
+                }
765
+            }
766
+        }
767
+        return $allGroups;
768
+    }
769
+
770
+    /**
771
+     * get a list of all users in a group
772
+     *
773
+     * @param string $gid
774
+     * @param string $search
775
+     * @param int $limit
776
+     * @param int $offset
777
+     * @return array with user ids
778
+     */
779
+    public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
780
+        if(!$this->enabled) {
781
+            return array();
782
+        }
783
+        if(!$this->groupExists($gid)) {
784
+            return array();
785
+        }
786
+        $search = $this->access->escapeFilterPart($search, true);
787
+        $cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
788
+        // check for cache of the exact query
789
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
790
+        if(!is_null($groupUsers)) {
791
+            return $groupUsers;
792
+        }
793
+
794
+        // check for cache of the query without limit and offset
795
+        $groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
796
+        if(!is_null($groupUsers)) {
797
+            $groupUsers = array_slice($groupUsers, $offset, $limit);
798
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
799
+            return $groupUsers;
800
+        }
801
+
802
+        if($limit === -1) {
803
+            $limit = null;
804
+        }
805
+        $groupDN = $this->access->groupname2dn($gid);
806
+        if(!$groupDN) {
807
+            // group couldn't be found, return empty resultset
808
+            $this->access->connection->writeToCache($cacheKey, array());
809
+            return array();
810
+        }
811
+
812
+        $primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
813
+        $posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
814
+        $members = array_keys($this->_groupMembers($groupDN));
815
+        if(!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
816
+            //in case users could not be retrieved, return empty result set
817
+            $this->access->connection->writeToCache($cacheKey, []);
818
+            return [];
819
+        }
820
+
821
+        $groupUsers = array();
822
+        $isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
823
+        $attrs = $this->access->userManager->getAttributes(true);
824
+        foreach($members as $member) {
825
+            if($isMemberUid) {
826
+                //we got uids, need to get their DNs to 'translate' them to user names
827
+                $filter = $this->access->combineFilterWithAnd(array(
828
+                    str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
829
+                    $this->access->getFilterPartForUserSearch($search)
830
+                ));
831
+                $ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
832
+                if(count($ldap_users) < 1) {
833
+                    continue;
834
+                }
835
+                $groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
836
+            } else {
837
+                //we got DNs, check if we need to filter by search or we can give back all of them
838
+                if ($search !== '') {
839
+                    if(!$this->access->readAttribute($member,
840
+                        $this->access->connection->ldapUserDisplayName,
841
+                        $this->access->getFilterPartForUserSearch($search))) {
842
+                        continue;
843
+                    }
844
+                }
845
+                // dn2username will also check if the users belong to the allowed base
846
+                if($ocname = $this->access->dn2username($member)) {
847
+                    $groupUsers[] = $ocname;
848
+                }
849
+            }
850
+        }
851
+
852
+        $groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
853
+        natsort($groupUsers);
854
+        $this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
855
+        $groupUsers = array_slice($groupUsers, $offset, $limit);
856
+
857
+        $this->access->connection->writeToCache($cacheKey, $groupUsers);
858
+
859
+        return $groupUsers;
860
+    }
861
+
862
+    /**
863
+     * returns the number of users in a group, who match the search term
864
+     * @param string $gid the internal group name
865
+     * @param string $search optional, a search string
866
+     * @return int|bool
867
+     */
868
+    public function countUsersInGroup($gid, $search = '') {
869
+        if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
870
+            return $this->groupPluginManager->countUsersInGroup($gid, $search);
871
+        }
872
+
873
+        $cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
874
+        if(!$this->enabled || !$this->groupExists($gid)) {
875
+            return false;
876
+        }
877
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
878
+        if(!is_null($groupUsers)) {
879
+            return $groupUsers;
880
+        }
881
+
882
+        $groupDN = $this->access->groupname2dn($gid);
883
+        if(!$groupDN) {
884
+            // group couldn't be found, return empty result set
885
+            $this->access->connection->writeToCache($cacheKey, false);
886
+            return false;
887
+        }
888
+
889
+        $members = array_keys($this->_groupMembers($groupDN));
890
+        $primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
891
+        if(!$members && $primaryUserCount === 0) {
892
+            //in case users could not be retrieved, return empty result set
893
+            $this->access->connection->writeToCache($cacheKey, false);
894
+            return false;
895
+        }
896
+
897
+        if ($search === '') {
898
+            $groupUsers = count($members) + $primaryUserCount;
899
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
900
+            return $groupUsers;
901
+        }
902
+        $search = $this->access->escapeFilterPart($search, true);
903
+        $isMemberUid =
904
+            (strtolower($this->access->connection->ldapGroupMemberAssocAttr)
905
+            === 'memberuid');
906
+
907
+        //we need to apply the search filter
908
+        //alternatives that need to be checked:
909
+        //a) get all users by search filter and array_intersect them
910
+        //b) a, but only when less than 1k 10k ?k users like it is
911
+        //c) put all DNs|uids in a LDAP filter, combine with the search string
912
+        //   and let it count.
913
+        //For now this is not important, because the only use of this method
914
+        //does not supply a search string
915
+        $groupUsers = array();
916
+        foreach($members as $member) {
917
+            if($isMemberUid) {
918
+                //we got uids, need to get their DNs to 'translate' them to user names
919
+                $filter = $this->access->combineFilterWithAnd(array(
920
+                    str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
921
+                    $this->access->getFilterPartForUserSearch($search)
922
+                ));
923
+                $ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
924
+                if(count($ldap_users) < 1) {
925
+                    continue;
926
+                }
927
+                $groupUsers[] = $this->access->dn2username($ldap_users[0]);
928
+            } else {
929
+                //we need to apply the search filter now
930
+                if(!$this->access->readAttribute($member,
931
+                    $this->access->connection->ldapUserDisplayName,
932
+                    $this->access->getFilterPartForUserSearch($search))) {
933
+                    continue;
934
+                }
935
+                // dn2username will also check if the users belong to the allowed base
936
+                if($ocname = $this->access->dn2username($member)) {
937
+                    $groupUsers[] = $ocname;
938
+                }
939
+            }
940
+        }
941
+
942
+        //and get users that have the group as primary
943
+        $primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
944
+
945
+        return count($groupUsers) + $primaryUsers;
946
+    }
947
+
948
+    /**
949
+     * get a list of all groups
950
+     *
951
+     * @param string $search
952
+     * @param $limit
953
+     * @param int $offset
954
+     * @return array with group names
955
+     *
956
+     * Returns a list with all groups (used by getGroups)
957
+     */
958
+    protected function getGroupsChunk($search = '', $limit = -1, $offset = 0) {
959
+        if(!$this->enabled) {
960
+            return array();
961
+        }
962
+        $cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
963
+
964
+        //Check cache before driving unnecessary searches
965
+        \OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, ILogger::DEBUG);
966
+        $ldap_groups = $this->access->connection->getFromCache($cacheKey);
967
+        if(!is_null($ldap_groups)) {
968
+            return $ldap_groups;
969
+        }
970
+
971
+        // if we'd pass -1 to LDAP search, we'd end up in a Protocol
972
+        // error. With a limit of 0, we get 0 results. So we pass null.
973
+        if($limit <= 0) {
974
+            $limit = null;
975
+        }
976
+        $filter = $this->access->combineFilterWithAnd(array(
977
+            $this->access->connection->ldapGroupFilter,
978
+            $this->access->getFilterPartForGroupSearch($search)
979
+        ));
980
+        \OCP\Util::writeLog('user_ldap', 'getGroups Filter '.$filter, ILogger::DEBUG);
981
+        $ldap_groups = $this->access->fetchListOfGroups($filter,
982
+                array($this->access->connection->ldapGroupDisplayName, 'dn'),
983
+                $limit,
984
+                $offset);
985
+        $ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
986
+
987
+        $this->access->connection->writeToCache($cacheKey, $ldap_groups);
988
+        return $ldap_groups;
989
+    }
990
+
991
+    /**
992
+     * get a list of all groups using a paged search
993
+     *
994
+     * @param string $search
995
+     * @param int $limit
996
+     * @param int $offset
997
+     * @return array with group names
998
+     *
999
+     * Returns a list with all groups
1000
+     * Uses a paged search if available to override a
1001
+     * server side search limit.
1002
+     * (active directory has a limit of 1000 by default)
1003
+     */
1004
+    public function getGroups($search = '', $limit = -1, $offset = 0) {
1005
+        if(!$this->enabled) {
1006
+            return array();
1007
+        }
1008
+        $search = $this->access->escapeFilterPart($search, true);
1009
+        $pagingSize = (int)$this->access->connection->ldapPagingSize;
1010
+        if (!$this->access->connection->hasPagedResultSupport || $pagingSize <= 0) {
1011
+            return $this->getGroupsChunk($search, $limit, $offset);
1012
+        }
1013
+        $maxGroups = 100000; // limit max results (just for safety reasons)
1014
+        if ($limit > -1) {
1015
+            $overallLimit = min($limit + $offset, $maxGroups);
1016
+        } else {
1017
+            $overallLimit = $maxGroups;
1018
+        }
1019
+        $chunkOffset = $offset;
1020
+        $allGroups = array();
1021
+        while ($chunkOffset < $overallLimit) {
1022
+            $chunkLimit = min($pagingSize, $overallLimit - $chunkOffset);
1023
+            $ldapGroups = $this->getGroupsChunk($search, $chunkLimit, $chunkOffset);
1024
+            $nread = count($ldapGroups);
1025
+            \OCP\Util::writeLog('user_ldap', 'getGroups('.$search.'): read '.$nread.' at offset '.$chunkOffset.' (limit: '.$chunkLimit.')', ILogger::DEBUG);
1026
+            if ($nread) {
1027
+                $allGroups = array_merge($allGroups, $ldapGroups);
1028
+                $chunkOffset += $nread;
1029
+            }
1030
+            if ($nread < $chunkLimit) {
1031
+                break;
1032
+            }
1033
+        }
1034
+        return $allGroups;
1035
+    }
1036
+
1037
+    /**
1038
+     * @param string $group
1039
+     * @return bool
1040
+     */
1041
+    public function groupMatchesFilter($group) {
1042
+        return (strripos($group, $this->groupSearch) !== false);
1043
+    }
1044
+
1045
+    /**
1046
+     * check if a group exists
1047
+     * @param string $gid
1048
+     * @return bool
1049
+     */
1050
+    public function groupExists($gid) {
1051
+        $groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1052
+        if(!is_null($groupExists)) {
1053
+            return (bool)$groupExists;
1054
+        }
1055
+
1056
+        //getting dn, if false the group does not exist. If dn, it may be mapped
1057
+        //only, requires more checking.
1058
+        $dn = $this->access->groupname2dn($gid);
1059
+        if(!$dn) {
1060
+            $this->access->connection->writeToCache('groupExists'.$gid, false);
1061
+            return false;
1062
+        }
1063
+
1064
+        //if group really still exists, we will be able to read its objectclass
1065
+        if(!is_array($this->access->readAttribute($dn, ''))) {
1066
+            $this->access->connection->writeToCache('groupExists'.$gid, false);
1067
+            return false;
1068
+        }
1069
+
1070
+        $this->access->connection->writeToCache('groupExists'.$gid, true);
1071
+        return true;
1072
+    }
1073
+
1074
+    /**
1075
+     * Check if backend implements actions
1076
+     * @param int $actions bitwise-or'ed actions
1077
+     * @return boolean
1078
+     *
1079
+     * Returns the supported actions as int to be
1080
+     * compared with GroupInterface::CREATE_GROUP etc.
1081
+     */
1082
+    public function implementsActions($actions) {
1083
+        return (bool)((GroupInterface::COUNT_USERS |
1084
+                $this->groupPluginManager->getImplementedActions()) & $actions);
1085
+    }
1086
+
1087
+    /**
1088
+     * Return access for LDAP interaction.
1089
+     * @return Access instance of Access for LDAP interaction
1090
+     */
1091
+    public function getLDAPAccess($gid) {
1092
+        return $this->access;
1093
+    }
1094
+
1095
+    /**
1096
+     * create a group
1097
+     * @param string $gid
1098
+     * @return bool
1099
+     * @throws \Exception
1100
+     */
1101
+    public function createGroup($gid) {
1102
+        if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1103
+            if ($dn = $this->groupPluginManager->createGroup($gid)) {
1104
+                //updates group mapping
1105
+                $this->access->dn2ocname($dn, $gid, false);
1106
+                $this->access->connection->writeToCache("groupExists".$gid, true);
1107
+            }
1108
+            return $dn != null;
1109
+        }
1110
+        throw new \Exception('Could not create group in LDAP backend.');
1111
+    }
1112
+
1113
+    /**
1114
+     * delete a group
1115
+     * @param string $gid gid of the group to delete
1116
+     * @return bool
1117
+     * @throws \Exception
1118
+     */
1119
+    public function deleteGroup($gid) {
1120
+        if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1121
+            if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1122
+                #delete group in nextcloud internal db
1123
+                $this->access->getGroupMapper()->unmap($gid);
1124
+                $this->access->connection->writeToCache("groupExists".$gid, false);
1125
+            }
1126
+            return $ret;
1127
+        }
1128
+        throw new \Exception('Could not delete group in LDAP backend.');
1129
+    }
1130
+
1131
+    /**
1132
+     * Add a user to a group
1133
+     * @param string $uid Name of the user to add to group
1134
+     * @param string $gid Name of the group in which add the user
1135
+     * @return bool
1136
+     * @throws \Exception
1137
+     */
1138
+    public function addToGroup($uid, $gid) {
1139
+        if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1140
+            if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1141
+                $this->access->connection->clearCache();
1142
+            }
1143
+            return $ret;
1144
+        }
1145
+        throw new \Exception('Could not add user to group in LDAP backend.');
1146
+    }
1147
+
1148
+    /**
1149
+     * Removes a user from a group
1150
+     * @param string $uid Name of the user to remove from group
1151
+     * @param string $gid Name of the group from which remove the user
1152
+     * @return bool
1153
+     * @throws \Exception
1154
+     */
1155
+    public function removeFromGroup($uid, $gid) {
1156
+        if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1157
+            if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1158
+                $this->access->connection->clearCache();
1159
+            }
1160
+            return $ret;
1161
+        }
1162
+        throw new \Exception('Could not remove user from group in LDAP backend.');
1163
+    }
1164
+
1165
+    /**
1166
+     * Gets group details
1167
+     * @param string $gid Name of the group
1168
+     * @return array | false
1169
+     * @throws \Exception
1170
+     */
1171
+    public function getGroupDetails($gid) {
1172
+        if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1173
+            return $this->groupPluginManager->getGroupDetails($gid);
1174
+        }
1175
+        throw new \Exception('Could not get group details in LDAP backend.');
1176
+    }
1177
+
1178
+    /**
1179
+     * Return LDAP connection resource from a cloned connection.
1180
+     * The cloned connection needs to be closed manually.
1181
+     * of the current access.
1182
+     * @param string $gid
1183
+     * @return resource of the LDAP connection
1184
+     */
1185
+    public function getNewLDAPConnection($gid) {
1186
+        $connection = clone $this->access->getConnection();
1187
+        return $connection->getConnectionResource();
1188
+    }
1189 1189
 
1190 1190
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Access.php 1 patch
Indentation   +1907 added lines, -1907 removed lines patch added patch discarded remove patch
@@ -61,1672 +61,1672 @@  discard block
 block discarded – undo
61 61
  * @package OCA\User_LDAP
62 62
  */
63 63
 class Access extends LDAPUtility implements IUserTools {
64
-	const UUID_ATTRIBUTES = ['entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid'];
65
-
66
-	/** @var \OCA\User_LDAP\Connection */
67
-	public $connection;
68
-	/** @var Manager */
69
-	public $userManager;
70
-	//never ever check this var directly, always use getPagedSearchResultState
71
-	protected $pagedSearchedSuccessful;
72
-
73
-	/**
74
-	 * @var string[] $cookies an array of returned Paged Result cookies
75
-	 */
76
-	protected $cookies = array();
77
-
78
-	/**
79
-	 * @var string $lastCookie the last cookie returned from a Paged Results
80
-	 * operation, defaults to an empty string
81
-	 */
82
-	protected $lastCookie = '';
83
-
84
-	/**
85
-	 * @var AbstractMapping $userMapper
86
-	 */
87
-	protected $userMapper;
88
-
89
-	/**
90
-	* @var AbstractMapping $userMapper
91
-	*/
92
-	protected $groupMapper;
93
-
94
-	/**
95
-	 * @var \OCA\User_LDAP\Helper
96
-	 */
97
-	private $helper;
98
-	/** @var IConfig */
99
-	private $config;
100
-	/** @var IUserManager */
101
-	private $ncUserManager;
102
-
103
-	public function __construct(
104
-		Connection $connection,
105
-		ILDAPWrapper $ldap,
106
-		Manager $userManager,
107
-		Helper $helper,
108
-		IConfig $config,
109
-		IUserManager $ncUserManager
110
-	) {
111
-		parent::__construct($ldap);
112
-		$this->connection = $connection;
113
-		$this->userManager = $userManager;
114
-		$this->userManager->setLdapAccess($this);
115
-		$this->helper = $helper;
116
-		$this->config = $config;
117
-		$this->ncUserManager = $ncUserManager;
118
-	}
119
-
120
-	/**
121
-	 * sets the User Mapper
122
-	 * @param AbstractMapping $mapper
123
-	 */
124
-	public function setUserMapper(AbstractMapping $mapper) {
125
-		$this->userMapper = $mapper;
126
-	}
127
-
128
-	/**
129
-	 * returns the User Mapper
130
-	 * @throws \Exception
131
-	 * @return AbstractMapping
132
-	 */
133
-	public function getUserMapper() {
134
-		if(is_null($this->userMapper)) {
135
-			throw new \Exception('UserMapper was not assigned to this Access instance.');
136
-		}
137
-		return $this->userMapper;
138
-	}
139
-
140
-	/**
141
-	 * sets the Group Mapper
142
-	 * @param AbstractMapping $mapper
143
-	 */
144
-	public function setGroupMapper(AbstractMapping $mapper) {
145
-		$this->groupMapper = $mapper;
146
-	}
147
-
148
-	/**
149
-	 * returns the Group Mapper
150
-	 * @throws \Exception
151
-	 * @return AbstractMapping
152
-	 */
153
-	public function getGroupMapper() {
154
-		if(is_null($this->groupMapper)) {
155
-			throw new \Exception('GroupMapper was not assigned to this Access instance.');
156
-		}
157
-		return $this->groupMapper;
158
-	}
159
-
160
-	/**
161
-	 * @return bool
162
-	 */
163
-	private function checkConnection() {
164
-		return ($this->connection instanceof Connection);
165
-	}
166
-
167
-	/**
168
-	 * returns the Connection instance
169
-	 * @return \OCA\User_LDAP\Connection
170
-	 */
171
-	public function getConnection() {
172
-		return $this->connection;
173
-	}
174
-
175
-	/**
176
-	 * reads a given attribute for an LDAP record identified by a DN
177
-	 *
178
-	 * @param string $dn the record in question
179
-	 * @param string $attr the attribute that shall be retrieved
180
-	 *        if empty, just check the record's existence
181
-	 * @param string $filter
182
-	 * @return array|false an array of values on success or an empty
183
-	 *          array if $attr is empty, false otherwise
184
-	 * @throws ServerNotAvailableException
185
-	 */
186
-	public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
187
-		if(!$this->checkConnection()) {
188
-			\OCP\Util::writeLog('user_ldap',
189
-				'No LDAP Connector assigned, access impossible for readAttribute.',
190
-				ILogger::WARN);
191
-			return false;
192
-		}
193
-		$cr = $this->connection->getConnectionResource();
194
-		if(!$this->ldap->isResource($cr)) {
195
-			//LDAP not available
196
-			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
197
-			return false;
198
-		}
199
-		//Cancel possibly running Paged Results operation, otherwise we run in
200
-		//LDAP protocol errors
201
-		$this->abandonPagedSearch();
202
-		// openLDAP requires that we init a new Paged Search. Not needed by AD,
203
-		// but does not hurt either.
204
-		$pagingSize = (int)$this->connection->ldapPagingSize;
205
-		// 0 won't result in replies, small numbers may leave out groups
206
-		// (cf. #12306), 500 is default for paging and should work everywhere.
207
-		$maxResults = $pagingSize > 20 ? $pagingSize : 500;
208
-		$attr = mb_strtolower($attr, 'UTF-8');
209
-		// the actual read attribute later may contain parameters on a ranged
210
-		// request, e.g. member;range=99-199. Depends on server reply.
211
-		$attrToRead = $attr;
212
-
213
-		$values = [];
214
-		$isRangeRequest = false;
215
-		do {
216
-			$result = $this->executeRead($cr, $dn, $attrToRead, $filter, $maxResults);
217
-			if(is_bool($result)) {
218
-				// when an exists request was run and it was successful, an empty
219
-				// array must be returned
220
-				return $result ? [] : false;
221
-			}
222
-
223
-			if (!$isRangeRequest) {
224
-				$values = $this->extractAttributeValuesFromResult($result, $attr);
225
-				if (!empty($values)) {
226
-					return $values;
227
-				}
228
-			}
229
-
230
-			$isRangeRequest = false;
231
-			$result = $this->extractRangeData($result, $attr);
232
-			if (!empty($result)) {
233
-				$normalizedResult = $this->extractAttributeValuesFromResult(
234
-					[ $attr => $result['values'] ],
235
-					$attr
236
-				);
237
-				$values = array_merge($values, $normalizedResult);
238
-
239
-				if($result['rangeHigh'] === '*') {
240
-					// when server replies with * as high range value, there are
241
-					// no more results left
242
-					return $values;
243
-				} else {
244
-					$low  = $result['rangeHigh'] + 1;
245
-					$attrToRead = $result['attributeName'] . ';range=' . $low . '-*';
246
-					$isRangeRequest = true;
247
-				}
248
-			}
249
-		} while($isRangeRequest);
250
-
251
-		\OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, ILogger::DEBUG);
252
-		return false;
253
-	}
254
-
255
-	/**
256
-	 * Runs an read operation against LDAP
257
-	 *
258
-	 * @param resource $cr the LDAP connection
259
-	 * @param string $dn
260
-	 * @param string $attribute
261
-	 * @param string $filter
262
-	 * @param int $maxResults
263
-	 * @return array|bool false if there was any error, true if an exists check
264
-	 *                    was performed and the requested DN found, array with the
265
-	 *                    returned data on a successful usual operation
266
-	 * @throws ServerNotAvailableException
267
-	 */
268
-	public function executeRead($cr, $dn, $attribute, $filter, $maxResults) {
269
-		$this->initPagedSearch($filter, array($dn), array($attribute), $maxResults, 0);
270
-		$dn = $this->helper->DNasBaseParameter($dn);
271
-		$rr = @$this->invokeLDAPMethod('read', $cr, $dn, $filter, array($attribute));
272
-		if (!$this->ldap->isResource($rr)) {
273
-			if ($attribute !== '') {
274
-				//do not throw this message on userExists check, irritates
275
-				\OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN ' . $dn, ILogger::DEBUG);
276
-			}
277
-			//in case an error occurs , e.g. object does not exist
278
-			return false;
279
-		}
280
-		if ($attribute === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $cr, $rr) === 1)) {
281
-			\OCP\Util::writeLog('user_ldap', 'readAttribute: ' . $dn . ' found', ILogger::DEBUG);
282
-			return true;
283
-		}
284
-		$er = $this->invokeLDAPMethod('firstEntry', $cr, $rr);
285
-		if (!$this->ldap->isResource($er)) {
286
-			//did not match the filter, return false
287
-			return false;
288
-		}
289
-		//LDAP attributes are not case sensitive
290
-		$result = \OCP\Util::mb_array_change_key_case(
291
-			$this->invokeLDAPMethod('getAttributes', $cr, $er), MB_CASE_LOWER, 'UTF-8');
292
-
293
-		return $result;
294
-	}
295
-
296
-	/**
297
-	 * Normalizes a result grom getAttributes(), i.e. handles DNs and binary
298
-	 * data if present.
299
-	 *
300
-	 * @param array $result from ILDAPWrapper::getAttributes()
301
-	 * @param string $attribute the attribute name that was read
302
-	 * @return string[]
303
-	 */
304
-	public function extractAttributeValuesFromResult($result, $attribute) {
305
-		$values = [];
306
-		if(isset($result[$attribute]) && $result[$attribute]['count'] > 0) {
307
-			$lowercaseAttribute = strtolower($attribute);
308
-			for($i=0;$i<$result[$attribute]['count'];$i++) {
309
-				if($this->resemblesDN($attribute)) {
310
-					$values[] = $this->helper->sanitizeDN($result[$attribute][$i]);
311
-				} elseif($lowercaseAttribute === 'objectguid' || $lowercaseAttribute === 'guid') {
312
-					$values[] = $this->convertObjectGUID2Str($result[$attribute][$i]);
313
-				} else {
314
-					$values[] = $result[$attribute][$i];
315
-				}
316
-			}
317
-		}
318
-		return $values;
319
-	}
320
-
321
-	/**
322
-	 * Attempts to find ranged data in a getAttribute results and extracts the
323
-	 * returned values as well as information on the range and full attribute
324
-	 * name for further processing.
325
-	 *
326
-	 * @param array $result from ILDAPWrapper::getAttributes()
327
-	 * @param string $attribute the attribute name that was read. Without ";range=…"
328
-	 * @return array If a range was detected with keys 'values', 'attributeName',
329
-	 *               'attributeFull' and 'rangeHigh', otherwise empty.
330
-	 */
331
-	public function extractRangeData($result, $attribute) {
332
-		$keys = array_keys($result);
333
-		foreach($keys as $key) {
334
-			if($key !== $attribute && strpos($key, $attribute) === 0) {
335
-				$queryData = explode(';', $key);
336
-				if(strpos($queryData[1], 'range=') === 0) {
337
-					$high = substr($queryData[1], 1 + strpos($queryData[1], '-'));
338
-					$data = [
339
-						'values' => $result[$key],
340
-						'attributeName' => $queryData[0],
341
-						'attributeFull' => $key,
342
-						'rangeHigh' => $high,
343
-					];
344
-					return $data;
345
-				}
346
-			}
347
-		}
348
-		return [];
349
-	}
64
+    const UUID_ATTRIBUTES = ['entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid'];
65
+
66
+    /** @var \OCA\User_LDAP\Connection */
67
+    public $connection;
68
+    /** @var Manager */
69
+    public $userManager;
70
+    //never ever check this var directly, always use getPagedSearchResultState
71
+    protected $pagedSearchedSuccessful;
72
+
73
+    /**
74
+     * @var string[] $cookies an array of returned Paged Result cookies
75
+     */
76
+    protected $cookies = array();
77
+
78
+    /**
79
+     * @var string $lastCookie the last cookie returned from a Paged Results
80
+     * operation, defaults to an empty string
81
+     */
82
+    protected $lastCookie = '';
83
+
84
+    /**
85
+     * @var AbstractMapping $userMapper
86
+     */
87
+    protected $userMapper;
88
+
89
+    /**
90
+     * @var AbstractMapping $userMapper
91
+     */
92
+    protected $groupMapper;
93
+
94
+    /**
95
+     * @var \OCA\User_LDAP\Helper
96
+     */
97
+    private $helper;
98
+    /** @var IConfig */
99
+    private $config;
100
+    /** @var IUserManager */
101
+    private $ncUserManager;
102
+
103
+    public function __construct(
104
+        Connection $connection,
105
+        ILDAPWrapper $ldap,
106
+        Manager $userManager,
107
+        Helper $helper,
108
+        IConfig $config,
109
+        IUserManager $ncUserManager
110
+    ) {
111
+        parent::__construct($ldap);
112
+        $this->connection = $connection;
113
+        $this->userManager = $userManager;
114
+        $this->userManager->setLdapAccess($this);
115
+        $this->helper = $helper;
116
+        $this->config = $config;
117
+        $this->ncUserManager = $ncUserManager;
118
+    }
119
+
120
+    /**
121
+     * sets the User Mapper
122
+     * @param AbstractMapping $mapper
123
+     */
124
+    public function setUserMapper(AbstractMapping $mapper) {
125
+        $this->userMapper = $mapper;
126
+    }
127
+
128
+    /**
129
+     * returns the User Mapper
130
+     * @throws \Exception
131
+     * @return AbstractMapping
132
+     */
133
+    public function getUserMapper() {
134
+        if(is_null($this->userMapper)) {
135
+            throw new \Exception('UserMapper was not assigned to this Access instance.');
136
+        }
137
+        return $this->userMapper;
138
+    }
139
+
140
+    /**
141
+     * sets the Group Mapper
142
+     * @param AbstractMapping $mapper
143
+     */
144
+    public function setGroupMapper(AbstractMapping $mapper) {
145
+        $this->groupMapper = $mapper;
146
+    }
147
+
148
+    /**
149
+     * returns the Group Mapper
150
+     * @throws \Exception
151
+     * @return AbstractMapping
152
+     */
153
+    public function getGroupMapper() {
154
+        if(is_null($this->groupMapper)) {
155
+            throw new \Exception('GroupMapper was not assigned to this Access instance.');
156
+        }
157
+        return $this->groupMapper;
158
+    }
159
+
160
+    /**
161
+     * @return bool
162
+     */
163
+    private function checkConnection() {
164
+        return ($this->connection instanceof Connection);
165
+    }
166
+
167
+    /**
168
+     * returns the Connection instance
169
+     * @return \OCA\User_LDAP\Connection
170
+     */
171
+    public function getConnection() {
172
+        return $this->connection;
173
+    }
174
+
175
+    /**
176
+     * reads a given attribute for an LDAP record identified by a DN
177
+     *
178
+     * @param string $dn the record in question
179
+     * @param string $attr the attribute that shall be retrieved
180
+     *        if empty, just check the record's existence
181
+     * @param string $filter
182
+     * @return array|false an array of values on success or an empty
183
+     *          array if $attr is empty, false otherwise
184
+     * @throws ServerNotAvailableException
185
+     */
186
+    public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
187
+        if(!$this->checkConnection()) {
188
+            \OCP\Util::writeLog('user_ldap',
189
+                'No LDAP Connector assigned, access impossible for readAttribute.',
190
+                ILogger::WARN);
191
+            return false;
192
+        }
193
+        $cr = $this->connection->getConnectionResource();
194
+        if(!$this->ldap->isResource($cr)) {
195
+            //LDAP not available
196
+            \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
197
+            return false;
198
+        }
199
+        //Cancel possibly running Paged Results operation, otherwise we run in
200
+        //LDAP protocol errors
201
+        $this->abandonPagedSearch();
202
+        // openLDAP requires that we init a new Paged Search. Not needed by AD,
203
+        // but does not hurt either.
204
+        $pagingSize = (int)$this->connection->ldapPagingSize;
205
+        // 0 won't result in replies, small numbers may leave out groups
206
+        // (cf. #12306), 500 is default for paging and should work everywhere.
207
+        $maxResults = $pagingSize > 20 ? $pagingSize : 500;
208
+        $attr = mb_strtolower($attr, 'UTF-8');
209
+        // the actual read attribute later may contain parameters on a ranged
210
+        // request, e.g. member;range=99-199. Depends on server reply.
211
+        $attrToRead = $attr;
212
+
213
+        $values = [];
214
+        $isRangeRequest = false;
215
+        do {
216
+            $result = $this->executeRead($cr, $dn, $attrToRead, $filter, $maxResults);
217
+            if(is_bool($result)) {
218
+                // when an exists request was run and it was successful, an empty
219
+                // array must be returned
220
+                return $result ? [] : false;
221
+            }
222
+
223
+            if (!$isRangeRequest) {
224
+                $values = $this->extractAttributeValuesFromResult($result, $attr);
225
+                if (!empty($values)) {
226
+                    return $values;
227
+                }
228
+            }
229
+
230
+            $isRangeRequest = false;
231
+            $result = $this->extractRangeData($result, $attr);
232
+            if (!empty($result)) {
233
+                $normalizedResult = $this->extractAttributeValuesFromResult(
234
+                    [ $attr => $result['values'] ],
235
+                    $attr
236
+                );
237
+                $values = array_merge($values, $normalizedResult);
238
+
239
+                if($result['rangeHigh'] === '*') {
240
+                    // when server replies with * as high range value, there are
241
+                    // no more results left
242
+                    return $values;
243
+                } else {
244
+                    $low  = $result['rangeHigh'] + 1;
245
+                    $attrToRead = $result['attributeName'] . ';range=' . $low . '-*';
246
+                    $isRangeRequest = true;
247
+                }
248
+            }
249
+        } while($isRangeRequest);
250
+
251
+        \OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, ILogger::DEBUG);
252
+        return false;
253
+    }
254
+
255
+    /**
256
+     * Runs an read operation against LDAP
257
+     *
258
+     * @param resource $cr the LDAP connection
259
+     * @param string $dn
260
+     * @param string $attribute
261
+     * @param string $filter
262
+     * @param int $maxResults
263
+     * @return array|bool false if there was any error, true if an exists check
264
+     *                    was performed and the requested DN found, array with the
265
+     *                    returned data on a successful usual operation
266
+     * @throws ServerNotAvailableException
267
+     */
268
+    public function executeRead($cr, $dn, $attribute, $filter, $maxResults) {
269
+        $this->initPagedSearch($filter, array($dn), array($attribute), $maxResults, 0);
270
+        $dn = $this->helper->DNasBaseParameter($dn);
271
+        $rr = @$this->invokeLDAPMethod('read', $cr, $dn, $filter, array($attribute));
272
+        if (!$this->ldap->isResource($rr)) {
273
+            if ($attribute !== '') {
274
+                //do not throw this message on userExists check, irritates
275
+                \OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN ' . $dn, ILogger::DEBUG);
276
+            }
277
+            //in case an error occurs , e.g. object does not exist
278
+            return false;
279
+        }
280
+        if ($attribute === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $cr, $rr) === 1)) {
281
+            \OCP\Util::writeLog('user_ldap', 'readAttribute: ' . $dn . ' found', ILogger::DEBUG);
282
+            return true;
283
+        }
284
+        $er = $this->invokeLDAPMethod('firstEntry', $cr, $rr);
285
+        if (!$this->ldap->isResource($er)) {
286
+            //did not match the filter, return false
287
+            return false;
288
+        }
289
+        //LDAP attributes are not case sensitive
290
+        $result = \OCP\Util::mb_array_change_key_case(
291
+            $this->invokeLDAPMethod('getAttributes', $cr, $er), MB_CASE_LOWER, 'UTF-8');
292
+
293
+        return $result;
294
+    }
295
+
296
+    /**
297
+     * Normalizes a result grom getAttributes(), i.e. handles DNs and binary
298
+     * data if present.
299
+     *
300
+     * @param array $result from ILDAPWrapper::getAttributes()
301
+     * @param string $attribute the attribute name that was read
302
+     * @return string[]
303
+     */
304
+    public function extractAttributeValuesFromResult($result, $attribute) {
305
+        $values = [];
306
+        if(isset($result[$attribute]) && $result[$attribute]['count'] > 0) {
307
+            $lowercaseAttribute = strtolower($attribute);
308
+            for($i=0;$i<$result[$attribute]['count'];$i++) {
309
+                if($this->resemblesDN($attribute)) {
310
+                    $values[] = $this->helper->sanitizeDN($result[$attribute][$i]);
311
+                } elseif($lowercaseAttribute === 'objectguid' || $lowercaseAttribute === 'guid') {
312
+                    $values[] = $this->convertObjectGUID2Str($result[$attribute][$i]);
313
+                } else {
314
+                    $values[] = $result[$attribute][$i];
315
+                }
316
+            }
317
+        }
318
+        return $values;
319
+    }
320
+
321
+    /**
322
+     * Attempts to find ranged data in a getAttribute results and extracts the
323
+     * returned values as well as information on the range and full attribute
324
+     * name for further processing.
325
+     *
326
+     * @param array $result from ILDAPWrapper::getAttributes()
327
+     * @param string $attribute the attribute name that was read. Without ";range=…"
328
+     * @return array If a range was detected with keys 'values', 'attributeName',
329
+     *               'attributeFull' and 'rangeHigh', otherwise empty.
330
+     */
331
+    public function extractRangeData($result, $attribute) {
332
+        $keys = array_keys($result);
333
+        foreach($keys as $key) {
334
+            if($key !== $attribute && strpos($key, $attribute) === 0) {
335
+                $queryData = explode(';', $key);
336
+                if(strpos($queryData[1], 'range=') === 0) {
337
+                    $high = substr($queryData[1], 1 + strpos($queryData[1], '-'));
338
+                    $data = [
339
+                        'values' => $result[$key],
340
+                        'attributeName' => $queryData[0],
341
+                        'attributeFull' => $key,
342
+                        'rangeHigh' => $high,
343
+                    ];
344
+                    return $data;
345
+                }
346
+            }
347
+        }
348
+        return [];
349
+    }
350 350
 	
351
-	/**
352
-	 * Set password for an LDAP user identified by a DN
353
-	 *
354
-	 * @param string $userDN the user in question
355
-	 * @param string $password the new password
356
-	 * @return bool
357
-	 * @throws HintException
358
-	 * @throws \Exception
359
-	 */
360
-	public function setPassword($userDN, $password) {
361
-		if((int)$this->connection->turnOnPasswordChange !== 1) {
362
-			throw new \Exception('LDAP password changes are disabled.');
363
-		}
364
-		$cr = $this->connection->getConnectionResource();
365
-		if(!$this->ldap->isResource($cr)) {
366
-			//LDAP not available
367
-			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
368
-			return false;
369
-		}
370
-		try {
371
-			return @$this->invokeLDAPMethod('modReplace', $cr, $userDN, $password);
372
-		} catch(ConstraintViolationException $e) {
373
-			throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ').$e->getMessage(), $e->getCode());
374
-		}
375
-	}
376
-
377
-	/**
378
-	 * checks whether the given attributes value is probably a DN
379
-	 * @param string $attr the attribute in question
380
-	 * @return boolean if so true, otherwise false
381
-	 */
382
-	private function resemblesDN($attr) {
383
-		$resemblingAttributes = array(
384
-			'dn',
385
-			'uniquemember',
386
-			'member',
387
-			// memberOf is an "operational" attribute, without a definition in any RFC
388
-			'memberof'
389
-		);
390
-		return in_array($attr, $resemblingAttributes);
391
-	}
392
-
393
-	/**
394
-	 * checks whether the given string is probably a DN
395
-	 * @param string $string
396
-	 * @return boolean
397
-	 */
398
-	public function stringResemblesDN($string) {
399
-		$r = $this->ldap->explodeDN($string, 0);
400
-		// if exploding a DN succeeds and does not end up in
401
-		// an empty array except for $r[count] being 0.
402
-		return (is_array($r) && count($r) > 1);
403
-	}
404
-
405
-	/**
406
-	 * returns a DN-string that is cleaned from not domain parts, e.g.
407
-	 * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
408
-	 * becomes dc=foobar,dc=server,dc=org
409
-	 * @param string $dn
410
-	 * @return string
411
-	 */
412
-	public function getDomainDNFromDN($dn) {
413
-		$allParts = $this->ldap->explodeDN($dn, 0);
414
-		if($allParts === false) {
415
-			//not a valid DN
416
-			return '';
417
-		}
418
-		$domainParts = array();
419
-		$dcFound = false;
420
-		foreach($allParts as $part) {
421
-			if(!$dcFound && strpos($part, 'dc=') === 0) {
422
-				$dcFound = true;
423
-			}
424
-			if($dcFound) {
425
-				$domainParts[] = $part;
426
-			}
427
-		}
428
-		return implode(',', $domainParts);
429
-	}
430
-
431
-	/**
432
-	 * returns the LDAP DN for the given internal Nextcloud name of the group
433
-	 * @param string $name the Nextcloud name in question
434
-	 * @return string|false LDAP DN on success, otherwise false
435
-	 */
436
-	public function groupname2dn($name) {
437
-		return $this->groupMapper->getDNByName($name);
438
-	}
439
-
440
-	/**
441
-	 * returns the LDAP DN for the given internal Nextcloud name of the user
442
-	 * @param string $name the Nextcloud name in question
443
-	 * @return string|false with the LDAP DN on success, otherwise false
444
-	 */
445
-	public function username2dn($name) {
446
-		$fdn = $this->userMapper->getDNByName($name);
447
-
448
-		//Check whether the DN belongs to the Base, to avoid issues on multi-
449
-		//server setups
450
-		if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
451
-			return $fdn;
452
-		}
453
-
454
-		return false;
455
-	}
456
-
457
-	/**
458
-	 * returns the internal Nextcloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
459
-	 * @param string $fdn the dn of the group object
460
-	 * @param string $ldapName optional, the display name of the object
461
-	 * @return string|false with the name to use in Nextcloud, false on DN outside of search DN
462
-	 */
463
-	public function dn2groupname($fdn, $ldapName = null) {
464
-		//To avoid bypassing the base DN settings under certain circumstances
465
-		//with the group support, check whether the provided DN matches one of
466
-		//the given Bases
467
-		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
468
-			return false;
469
-		}
470
-
471
-		return $this->dn2ocname($fdn, $ldapName, false);
472
-	}
473
-
474
-	/**
475
-	 * accepts an array of group DNs and tests whether they match the user
476
-	 * filter by doing read operations against the group entries. Returns an
477
-	 * array of DNs that match the filter.
478
-	 *
479
-	 * @param string[] $groupDNs
480
-	 * @return string[]
481
-	 * @throws ServerNotAvailableException
482
-	 */
483
-	public function groupsMatchFilter($groupDNs) {
484
-		$validGroupDNs = [];
485
-		foreach($groupDNs as $dn) {
486
-			$cacheKey = 'groupsMatchFilter-'.$dn;
487
-			$groupMatchFilter = $this->connection->getFromCache($cacheKey);
488
-			if(!is_null($groupMatchFilter)) {
489
-				if($groupMatchFilter) {
490
-					$validGroupDNs[] = $dn;
491
-				}
492
-				continue;
493
-			}
494
-
495
-			// Check the base DN first. If this is not met already, we don't
496
-			// need to ask the server at all.
497
-			if(!$this->isDNPartOfBase($dn, $this->connection->ldapBaseGroups)) {
498
-				$this->connection->writeToCache($cacheKey, false);
499
-				continue;
500
-			}
501
-
502
-			$result = $this->readAttribute($dn, '', $this->connection->ldapGroupFilter);
503
-			if(is_array($result)) {
504
-				$this->connection->writeToCache($cacheKey, true);
505
-				$validGroupDNs[] = $dn;
506
-			} else {
507
-				$this->connection->writeToCache($cacheKey, false);
508
-			}
509
-
510
-		}
511
-		return $validGroupDNs;
512
-	}
513
-
514
-	/**
515
-	 * returns the internal Nextcloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
516
-	 * @param string $dn the dn of the user object
517
-	 * @param string $ldapName optional, the display name of the object
518
-	 * @return string|false with with the name to use in Nextcloud
519
-	 */
520
-	public function dn2username($fdn, $ldapName = null) {
521
-		//To avoid bypassing the base DN settings under certain circumstances
522
-		//with the group support, check whether the provided DN matches one of
523
-		//the given Bases
524
-		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
525
-			return false;
526
-		}
527
-
528
-		return $this->dn2ocname($fdn, $ldapName, true);
529
-	}
530
-
531
-	/**
532
-	 * returns an internal Nextcloud name for the given LDAP DN, false on DN outside of search DN
533
-	 *
534
-	 * @param string $fdn the dn of the user object
535
-	 * @param string|null $ldapName optional, the display name of the object
536
-	 * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
537
-	 * @param bool|null $newlyMapped
538
-	 * @param array|null $record
539
-	 * @return false|string with with the name to use in Nextcloud
540
-	 * @throws \Exception
541
-	 */
542
-	public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, array $record = null) {
543
-		$newlyMapped = false;
544
-		if($isUser) {
545
-			$mapper = $this->getUserMapper();
546
-			$nameAttribute = $this->connection->ldapUserDisplayName;
547
-			$filter = $this->connection->ldapUserFilter;
548
-		} else {
549
-			$mapper = $this->getGroupMapper();
550
-			$nameAttribute = $this->connection->ldapGroupDisplayName;
551
-			$filter = $this->connection->ldapGroupFilter;
552
-		}
553
-
554
-		//let's try to retrieve the Nextcloud name from the mappings table
555
-		$ncName = $mapper->getNameByDN($fdn);
556
-		if(is_string($ncName)) {
557
-			return $ncName;
558
-		}
559
-
560
-		//second try: get the UUID and check if it is known. Then, update the DN and return the name.
561
-		$uuid = $this->getUUID($fdn, $isUser, $record);
562
-		if(is_string($uuid)) {
563
-			$ncName = $mapper->getNameByUUID($uuid);
564
-			if(is_string($ncName)) {
565
-				$mapper->setDNbyUUID($fdn, $uuid);
566
-				return $ncName;
567
-			}
568
-		} else {
569
-			//If the UUID can't be detected something is foul.
570
-			\OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', ILogger::INFO);
571
-			return false;
572
-		}
573
-
574
-		if(is_null($ldapName)) {
575
-			$ldapName = $this->readAttribute($fdn, $nameAttribute, $filter);
576
-			if(!isset($ldapName[0]) && empty($ldapName[0])) {
577
-				\OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.' with filter '.$filter.'.', ILogger::INFO);
578
-				return false;
579
-			}
580
-			$ldapName = $ldapName[0];
581
-		}
582
-
583
-		if($isUser) {
584
-			$usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr;
585
-			if ($usernameAttribute !== '') {
586
-				$username = $this->readAttribute($fdn, $usernameAttribute);
587
-				$username = $username[0];
588
-			} else {
589
-				$username = $uuid;
590
-			}
591
-			try {
592
-				$intName = $this->sanitizeUsername($username);
593
-			} catch (\InvalidArgumentException $e) {
594
-				\OC::$server->getLogger()->logException($e, [
595
-					'app' => 'user_ldap',
596
-					'level' => ILogger::WARN,
597
-				]);
598
-				// we don't attempt to set a username here. We can go for
599
-				// for an alternative 4 digit random number as we would append
600
-				// otherwise, however it's likely not enough space in bigger
601
-				// setups, and most importantly: this is not intended.
602
-				return false;
603
-			}
604
-		} else {
605
-			$intName = $ldapName;
606
-		}
607
-
608
-		//a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
609
-		//disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
610
-		//NOTE: mind, disabling cache affects only this instance! Using it
611
-		// outside of core user management will still cache the user as non-existing.
612
-		$originalTTL = $this->connection->ldapCacheTTL;
613
-		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
614
-		if(($isUser && $intName !== '' && !$this->ncUserManager->userExists($intName))
615
-			|| (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))) {
616
-			if($mapper->map($fdn, $intName, $uuid)) {
617
-				$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
618
-				if($this->ncUserManager instanceof PublicEmitter) {
619
-					$this->ncUserManager->emit('\OC\User', 'assignedUserId', [$intName]);
620
-				}
621
-				$newlyMapped = true;
622
-				return $intName;
623
-			}
624
-		}
625
-		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
626
-
627
-		$altName = $this->createAltInternalOwnCloudName($intName, $isUser);
628
-		if(is_string($altName) && $mapper->map($fdn, $altName, $uuid)) {
629
-			if($this->ncUserManager instanceof PublicEmitter) {
630
-				$this->ncUserManager->emit('\OC\User', 'assignedUserId', [$intName]);
631
-			}
632
-			$newlyMapped = true;
633
-			return $altName;
634
-		}
635
-
636
-		//if everything else did not help..
637
-		\OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', ILogger::INFO);
638
-		return false;
639
-	}
640
-
641
-	/**
642
-	 * gives back the user names as they are used ownClod internally
643
-	 * @param array $ldapUsers as returned by fetchList()
644
-	 * @return array an array with the user names to use in Nextcloud
645
-	 *
646
-	 * gives back the user names as they are used ownClod internally
647
-	 */
648
-	public function nextcloudUserNames($ldapUsers) {
649
-		return $this->ldap2NextcloudNames($ldapUsers, true);
650
-	}
651
-
652
-	/**
653
-	 * gives back the group names as they are used ownClod internally
654
-	 * @param array $ldapGroups as returned by fetchList()
655
-	 * @return array an array with the group names to use in Nextcloud
656
-	 *
657
-	 * gives back the group names as they are used ownClod internally
658
-	 */
659
-	public function nextcloudGroupNames($ldapGroups) {
660
-		return $this->ldap2NextcloudNames($ldapGroups, false);
661
-	}
662
-
663
-	/**
664
-	 * @param array $ldapObjects as returned by fetchList()
665
-	 * @param bool $isUsers
666
-	 * @return array
667
-	 */
668
-	private function ldap2NextcloudNames($ldapObjects, $isUsers) {
669
-		if($isUsers) {
670
-			$nameAttribute = $this->connection->ldapUserDisplayName;
671
-			$sndAttribute  = $this->connection->ldapUserDisplayName2;
672
-		} else {
673
-			$nameAttribute = $this->connection->ldapGroupDisplayName;
674
-		}
675
-		$nextcloudNames = array();
676
-
677
-		foreach($ldapObjects as $ldapObject) {
678
-			$nameByLDAP = null;
679
-			if(    isset($ldapObject[$nameAttribute])
680
-				&& is_array($ldapObject[$nameAttribute])
681
-				&& isset($ldapObject[$nameAttribute][0])
682
-			) {
683
-				// might be set, but not necessarily. if so, we use it.
684
-				$nameByLDAP = $ldapObject[$nameAttribute][0];
685
-			}
686
-
687
-			$ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
688
-			if($ncName) {
689
-				$nextcloudNames[] = $ncName;
690
-				if($isUsers) {
691
-					//cache the user names so it does not need to be retrieved
692
-					//again later (e.g. sharing dialogue).
693
-					if(is_null($nameByLDAP)) {
694
-						continue;
695
-					}
696
-					$sndName = isset($ldapObject[$sndAttribute][0])
697
-						? $ldapObject[$sndAttribute][0] : '';
698
-					$this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName);
699
-				}
700
-			}
701
-		}
702
-		return $nextcloudNames;
703
-	}
704
-
705
-	/**
706
-	 * caches the user display name
707
-	 * @param string $ocName the internal Nextcloud username
708
-	 * @param string|false $home the home directory path
709
-	 */
710
-	public function cacheUserHome($ocName, $home) {
711
-		$cacheKey = 'getHome'.$ocName;
712
-		$this->connection->writeToCache($cacheKey, $home);
713
-	}
714
-
715
-	/**
716
-	 * caches a user as existing
717
-	 * @param string $ocName the internal Nextcloud username
718
-	 */
719
-	public function cacheUserExists($ocName) {
720
-		$this->connection->writeToCache('userExists'.$ocName, true);
721
-	}
722
-
723
-	/**
724
-	 * caches the user display name
725
-	 * @param string $ocName the internal Nextcloud username
726
-	 * @param string $displayName the display name
727
-	 * @param string $displayName2 the second display name
728
-	 */
729
-	public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
730
-		$user = $this->userManager->get($ocName);
731
-		if($user === null) {
732
-			return;
733
-		}
734
-		$displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
735
-		$cacheKeyTrunk = 'getDisplayName';
736
-		$this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
737
-	}
738
-
739
-	/**
740
-	 * creates a unique name for internal Nextcloud use for users. Don't call it directly.
741
-	 * @param string $name the display name of the object
742
-	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
743
-	 *
744
-	 * Instead of using this method directly, call
745
-	 * createAltInternalOwnCloudName($name, true)
746
-	 */
747
-	private function _createAltInternalOwnCloudNameForUsers($name) {
748
-		$attempts = 0;
749
-		//while loop is just a precaution. If a name is not generated within
750
-		//20 attempts, something else is very wrong. Avoids infinite loop.
751
-		while($attempts < 20){
752
-			$altName = $name . '_' . rand(1000,9999);
753
-			if(!$this->ncUserManager->userExists($altName)) {
754
-				return $altName;
755
-			}
756
-			$attempts++;
757
-		}
758
-		return false;
759
-	}
760
-
761
-	/**
762
-	 * creates a unique name for internal Nextcloud use for groups. Don't call it directly.
763
-	 * @param string $name the display name of the object
764
-	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful.
765
-	 *
766
-	 * Instead of using this method directly, call
767
-	 * createAltInternalOwnCloudName($name, false)
768
-	 *
769
-	 * Group names are also used as display names, so we do a sequential
770
-	 * numbering, e.g. Developers_42 when there are 41 other groups called
771
-	 * "Developers"
772
-	 */
773
-	private function _createAltInternalOwnCloudNameForGroups($name) {
774
-		$usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
775
-		if(!$usedNames || count($usedNames) === 0) {
776
-			$lastNo = 1; //will become name_2
777
-		} else {
778
-			natsort($usedNames);
779
-			$lastName = array_pop($usedNames);
780
-			$lastNo = (int)substr($lastName, strrpos($lastName, '_') + 1);
781
-		}
782
-		$altName = $name.'_'. (string)($lastNo+1);
783
-		unset($usedNames);
784
-
785
-		$attempts = 1;
786
-		while($attempts < 21){
787
-			// Check to be really sure it is unique
788
-			// while loop is just a precaution. If a name is not generated within
789
-			// 20 attempts, something else is very wrong. Avoids infinite loop.
790
-			if(!\OC::$server->getGroupManager()->groupExists($altName)) {
791
-				return $altName;
792
-			}
793
-			$altName = $name . '_' . ($lastNo + $attempts);
794
-			$attempts++;
795
-		}
796
-		return false;
797
-	}
798
-
799
-	/**
800
-	 * creates a unique name for internal Nextcloud use.
801
-	 * @param string $name the display name of the object
802
-	 * @param boolean $isUser whether name should be created for a user (true) or a group (false)
803
-	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
804
-	 */
805
-	private function createAltInternalOwnCloudName($name, $isUser) {
806
-		$originalTTL = $this->connection->ldapCacheTTL;
807
-		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
808
-		if($isUser) {
809
-			$altName = $this->_createAltInternalOwnCloudNameForUsers($name);
810
-		} else {
811
-			$altName = $this->_createAltInternalOwnCloudNameForGroups($name);
812
-		}
813
-		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
814
-
815
-		return $altName;
816
-	}
817
-
818
-	/**
819
-	 * fetches a list of users according to a provided loginName and utilizing
820
-	 * the login filter.
821
-	 *
822
-	 * @param string $loginName
823
-	 * @param array $attributes optional, list of attributes to read
824
-	 * @return array
825
-	 */
826
-	public function fetchUsersByLoginName($loginName, $attributes = array('dn')) {
827
-		$loginName = $this->escapeFilterPart($loginName);
828
-		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
829
-		return $this->fetchListOfUsers($filter, $attributes);
830
-	}
831
-
832
-	/**
833
-	 * counts the number of users according to a provided loginName and
834
-	 * utilizing the login filter.
835
-	 *
836
-	 * @param string $loginName
837
-	 * @return int
838
-	 */
839
-	public function countUsersByLoginName($loginName) {
840
-		$loginName = $this->escapeFilterPart($loginName);
841
-		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
842
-		return $this->countUsers($filter);
843
-	}
844
-
845
-	/**
846
-	 * @param string $filter
847
-	 * @param string|string[] $attr
848
-	 * @param int $limit
849
-	 * @param int $offset
850
-	 * @param bool $forceApplyAttributes
851
-	 * @return array
852
-	 */
853
-	public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null, $forceApplyAttributes = false) {
854
-		$ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
855
-		$recordsToUpdate = $ldapRecords;
856
-		if(!$forceApplyAttributes) {
857
-			$isBackgroundJobModeAjax = $this->config
858
-					->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
859
-			$recordsToUpdate = array_filter($ldapRecords, function($record) use ($isBackgroundJobModeAjax) {
860
-				$newlyMapped = false;
861
-				$uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record);
862
-				if(is_string($uid)) {
863
-					$this->cacheUserExists($uid);
864
-				}
865
-				return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax);
866
-			});
867
-		}
868
-		$this->batchApplyUserAttributes($recordsToUpdate);
869
-		return $this->fetchList($ldapRecords, count($attr) > 1);
870
-	}
871
-
872
-	/**
873
-	 * provided with an array of LDAP user records the method will fetch the
874
-	 * user object and requests it to process the freshly fetched attributes and
875
-	 * and their values
876
-	 * @param array $ldapRecords
877
-	 */
878
-	public function batchApplyUserAttributes(array $ldapRecords){
879
-		$displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
880
-		foreach($ldapRecords as $userRecord) {
881
-			if(!isset($userRecord[$displayNameAttribute])) {
882
-				// displayName is obligatory
883
-				continue;
884
-			}
885
-			$ocName  = $this->dn2ocname($userRecord['dn'][0], null, true);
886
-			if($ocName === false) {
887
-				continue;
888
-			}
889
-			$user = $this->userManager->get($ocName);
890
-			if($user instanceof OfflineUser) {
891
-				$user->unmark();
892
-				$user = $this->userManager->get($ocName);
893
-			}
894
-			if ($user !== null) {
895
-				$user->processAttributes($userRecord);
896
-			} else {
897
-				\OC::$server->getLogger()->debug(
898
-					"The ldap user manager returned null for $ocName",
899
-					['app'=>'user_ldap']
900
-				);
901
-			}
902
-		}
903
-	}
904
-
905
-	/**
906
-	 * @param string $filter
907
-	 * @param string|string[] $attr
908
-	 * @param int $limit
909
-	 * @param int $offset
910
-	 * @return array
911
-	 */
912
-	public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
913
-		return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), count($attr) > 1);
914
-	}
915
-
916
-	/**
917
-	 * @param array $list
918
-	 * @param bool $manyAttributes
919
-	 * @return array
920
-	 */
921
-	private function fetchList($list, $manyAttributes) {
922
-		if(is_array($list)) {
923
-			if($manyAttributes) {
924
-				return $list;
925
-			} else {
926
-				$list = array_reduce($list, function($carry, $item) {
927
-					$attribute = array_keys($item)[0];
928
-					$carry[] = $item[$attribute][0];
929
-					return $carry;
930
-				}, array());
931
-				return array_unique($list, SORT_LOCALE_STRING);
932
-			}
933
-		}
934
-
935
-		//error cause actually, maybe throw an exception in future.
936
-		return array();
937
-	}
938
-
939
-	/**
940
-	 * executes an LDAP search, optimized for Users
941
-	 * @param string $filter the LDAP filter for the search
942
-	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
943
-	 * @param integer $limit
944
-	 * @param integer $offset
945
-	 * @return array with the search result
946
-	 *
947
-	 * Executes an LDAP search
948
-	 */
949
-	public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
950
-		return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
951
-	}
952
-
953
-	/**
954
-	 * @param string $filter
955
-	 * @param string|string[] $attr
956
-	 * @param int $limit
957
-	 * @param int $offset
958
-	 * @return false|int
959
-	 */
960
-	public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
961
-		return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
962
-	}
963
-
964
-	/**
965
-	 * executes an LDAP search, optimized for Groups
966
-	 * @param string $filter the LDAP filter for the search
967
-	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
968
-	 * @param integer $limit
969
-	 * @param integer $offset
970
-	 * @return array with the search result
971
-	 *
972
-	 * Executes an LDAP search
973
-	 */
974
-	public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
975
-		return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
976
-	}
977
-
978
-	/**
979
-	 * returns the number of available groups
980
-	 * @param string $filter the LDAP search filter
981
-	 * @param string[] $attr optional
982
-	 * @param int|null $limit
983
-	 * @param int|null $offset
984
-	 * @return int|bool
985
-	 */
986
-	public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
987
-		return $this->count($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
988
-	}
989
-
990
-	/**
991
-	 * returns the number of available objects on the base DN
992
-	 *
993
-	 * @param int|null $limit
994
-	 * @param int|null $offset
995
-	 * @return int|bool
996
-	 */
997
-	public function countObjects($limit = null, $offset = null) {
998
-		return $this->count('objectclass=*', $this->connection->ldapBase, array('dn'), $limit, $offset);
999
-	}
1000
-
1001
-	/**
1002
-	 * Returns the LDAP handler
1003
-	 * @throws \OC\ServerNotAvailableException
1004
-	 */
1005
-
1006
-	/**
1007
-	 * @return mixed
1008
-	 * @throws \OC\ServerNotAvailableException
1009
-	 */
1010
-	private function invokeLDAPMethod() {
1011
-		$arguments = func_get_args();
1012
-		$command = array_shift($arguments);
1013
-		$cr = array_shift($arguments);
1014
-		if (!method_exists($this->ldap, $command)) {
1015
-			return null;
1016
-		}
1017
-		array_unshift($arguments, $cr);
1018
-		// php no longer supports call-time pass-by-reference
1019
-		// thus cannot support controlPagedResultResponse as the third argument
1020
-		// is a reference
1021
-		$doMethod = function () use ($command, &$arguments) {
1022
-			if ($command == 'controlPagedResultResponse') {
1023
-				throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
1024
-			} else {
1025
-				return call_user_func_array(array($this->ldap, $command), $arguments);
1026
-			}
1027
-		};
1028
-		try {
1029
-			$ret = $doMethod();
1030
-		} catch (ServerNotAvailableException $e) {
1031
-			/* Server connection lost, attempt to reestablish it
351
+    /**
352
+     * Set password for an LDAP user identified by a DN
353
+     *
354
+     * @param string $userDN the user in question
355
+     * @param string $password the new password
356
+     * @return bool
357
+     * @throws HintException
358
+     * @throws \Exception
359
+     */
360
+    public function setPassword($userDN, $password) {
361
+        if((int)$this->connection->turnOnPasswordChange !== 1) {
362
+            throw new \Exception('LDAP password changes are disabled.');
363
+        }
364
+        $cr = $this->connection->getConnectionResource();
365
+        if(!$this->ldap->isResource($cr)) {
366
+            //LDAP not available
367
+            \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
368
+            return false;
369
+        }
370
+        try {
371
+            return @$this->invokeLDAPMethod('modReplace', $cr, $userDN, $password);
372
+        } catch(ConstraintViolationException $e) {
373
+            throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ').$e->getMessage(), $e->getCode());
374
+        }
375
+    }
376
+
377
+    /**
378
+     * checks whether the given attributes value is probably a DN
379
+     * @param string $attr the attribute in question
380
+     * @return boolean if so true, otherwise false
381
+     */
382
+    private function resemblesDN($attr) {
383
+        $resemblingAttributes = array(
384
+            'dn',
385
+            'uniquemember',
386
+            'member',
387
+            // memberOf is an "operational" attribute, without a definition in any RFC
388
+            'memberof'
389
+        );
390
+        return in_array($attr, $resemblingAttributes);
391
+    }
392
+
393
+    /**
394
+     * checks whether the given string is probably a DN
395
+     * @param string $string
396
+     * @return boolean
397
+     */
398
+    public function stringResemblesDN($string) {
399
+        $r = $this->ldap->explodeDN($string, 0);
400
+        // if exploding a DN succeeds and does not end up in
401
+        // an empty array except for $r[count] being 0.
402
+        return (is_array($r) && count($r) > 1);
403
+    }
404
+
405
+    /**
406
+     * returns a DN-string that is cleaned from not domain parts, e.g.
407
+     * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
408
+     * becomes dc=foobar,dc=server,dc=org
409
+     * @param string $dn
410
+     * @return string
411
+     */
412
+    public function getDomainDNFromDN($dn) {
413
+        $allParts = $this->ldap->explodeDN($dn, 0);
414
+        if($allParts === false) {
415
+            //not a valid DN
416
+            return '';
417
+        }
418
+        $domainParts = array();
419
+        $dcFound = false;
420
+        foreach($allParts as $part) {
421
+            if(!$dcFound && strpos($part, 'dc=') === 0) {
422
+                $dcFound = true;
423
+            }
424
+            if($dcFound) {
425
+                $domainParts[] = $part;
426
+            }
427
+        }
428
+        return implode(',', $domainParts);
429
+    }
430
+
431
+    /**
432
+     * returns the LDAP DN for the given internal Nextcloud name of the group
433
+     * @param string $name the Nextcloud name in question
434
+     * @return string|false LDAP DN on success, otherwise false
435
+     */
436
+    public function groupname2dn($name) {
437
+        return $this->groupMapper->getDNByName($name);
438
+    }
439
+
440
+    /**
441
+     * returns the LDAP DN for the given internal Nextcloud name of the user
442
+     * @param string $name the Nextcloud name in question
443
+     * @return string|false with the LDAP DN on success, otherwise false
444
+     */
445
+    public function username2dn($name) {
446
+        $fdn = $this->userMapper->getDNByName($name);
447
+
448
+        //Check whether the DN belongs to the Base, to avoid issues on multi-
449
+        //server setups
450
+        if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
451
+            return $fdn;
452
+        }
453
+
454
+        return false;
455
+    }
456
+
457
+    /**
458
+     * returns the internal Nextcloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
459
+     * @param string $fdn the dn of the group object
460
+     * @param string $ldapName optional, the display name of the object
461
+     * @return string|false with the name to use in Nextcloud, false on DN outside of search DN
462
+     */
463
+    public function dn2groupname($fdn, $ldapName = null) {
464
+        //To avoid bypassing the base DN settings under certain circumstances
465
+        //with the group support, check whether the provided DN matches one of
466
+        //the given Bases
467
+        if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
468
+            return false;
469
+        }
470
+
471
+        return $this->dn2ocname($fdn, $ldapName, false);
472
+    }
473
+
474
+    /**
475
+     * accepts an array of group DNs and tests whether they match the user
476
+     * filter by doing read operations against the group entries. Returns an
477
+     * array of DNs that match the filter.
478
+     *
479
+     * @param string[] $groupDNs
480
+     * @return string[]
481
+     * @throws ServerNotAvailableException
482
+     */
483
+    public function groupsMatchFilter($groupDNs) {
484
+        $validGroupDNs = [];
485
+        foreach($groupDNs as $dn) {
486
+            $cacheKey = 'groupsMatchFilter-'.$dn;
487
+            $groupMatchFilter = $this->connection->getFromCache($cacheKey);
488
+            if(!is_null($groupMatchFilter)) {
489
+                if($groupMatchFilter) {
490
+                    $validGroupDNs[] = $dn;
491
+                }
492
+                continue;
493
+            }
494
+
495
+            // Check the base DN first. If this is not met already, we don't
496
+            // need to ask the server at all.
497
+            if(!$this->isDNPartOfBase($dn, $this->connection->ldapBaseGroups)) {
498
+                $this->connection->writeToCache($cacheKey, false);
499
+                continue;
500
+            }
501
+
502
+            $result = $this->readAttribute($dn, '', $this->connection->ldapGroupFilter);
503
+            if(is_array($result)) {
504
+                $this->connection->writeToCache($cacheKey, true);
505
+                $validGroupDNs[] = $dn;
506
+            } else {
507
+                $this->connection->writeToCache($cacheKey, false);
508
+            }
509
+
510
+        }
511
+        return $validGroupDNs;
512
+    }
513
+
514
+    /**
515
+     * returns the internal Nextcloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
516
+     * @param string $dn the dn of the user object
517
+     * @param string $ldapName optional, the display name of the object
518
+     * @return string|false with with the name to use in Nextcloud
519
+     */
520
+    public function dn2username($fdn, $ldapName = null) {
521
+        //To avoid bypassing the base DN settings under certain circumstances
522
+        //with the group support, check whether the provided DN matches one of
523
+        //the given Bases
524
+        if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
525
+            return false;
526
+        }
527
+
528
+        return $this->dn2ocname($fdn, $ldapName, true);
529
+    }
530
+
531
+    /**
532
+     * returns an internal Nextcloud name for the given LDAP DN, false on DN outside of search DN
533
+     *
534
+     * @param string $fdn the dn of the user object
535
+     * @param string|null $ldapName optional, the display name of the object
536
+     * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
537
+     * @param bool|null $newlyMapped
538
+     * @param array|null $record
539
+     * @return false|string with with the name to use in Nextcloud
540
+     * @throws \Exception
541
+     */
542
+    public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, array $record = null) {
543
+        $newlyMapped = false;
544
+        if($isUser) {
545
+            $mapper = $this->getUserMapper();
546
+            $nameAttribute = $this->connection->ldapUserDisplayName;
547
+            $filter = $this->connection->ldapUserFilter;
548
+        } else {
549
+            $mapper = $this->getGroupMapper();
550
+            $nameAttribute = $this->connection->ldapGroupDisplayName;
551
+            $filter = $this->connection->ldapGroupFilter;
552
+        }
553
+
554
+        //let's try to retrieve the Nextcloud name from the mappings table
555
+        $ncName = $mapper->getNameByDN($fdn);
556
+        if(is_string($ncName)) {
557
+            return $ncName;
558
+        }
559
+
560
+        //second try: get the UUID and check if it is known. Then, update the DN and return the name.
561
+        $uuid = $this->getUUID($fdn, $isUser, $record);
562
+        if(is_string($uuid)) {
563
+            $ncName = $mapper->getNameByUUID($uuid);
564
+            if(is_string($ncName)) {
565
+                $mapper->setDNbyUUID($fdn, $uuid);
566
+                return $ncName;
567
+            }
568
+        } else {
569
+            //If the UUID can't be detected something is foul.
570
+            \OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', ILogger::INFO);
571
+            return false;
572
+        }
573
+
574
+        if(is_null($ldapName)) {
575
+            $ldapName = $this->readAttribute($fdn, $nameAttribute, $filter);
576
+            if(!isset($ldapName[0]) && empty($ldapName[0])) {
577
+                \OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.' with filter '.$filter.'.', ILogger::INFO);
578
+                return false;
579
+            }
580
+            $ldapName = $ldapName[0];
581
+        }
582
+
583
+        if($isUser) {
584
+            $usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr;
585
+            if ($usernameAttribute !== '') {
586
+                $username = $this->readAttribute($fdn, $usernameAttribute);
587
+                $username = $username[0];
588
+            } else {
589
+                $username = $uuid;
590
+            }
591
+            try {
592
+                $intName = $this->sanitizeUsername($username);
593
+            } catch (\InvalidArgumentException $e) {
594
+                \OC::$server->getLogger()->logException($e, [
595
+                    'app' => 'user_ldap',
596
+                    'level' => ILogger::WARN,
597
+                ]);
598
+                // we don't attempt to set a username here. We can go for
599
+                // for an alternative 4 digit random number as we would append
600
+                // otherwise, however it's likely not enough space in bigger
601
+                // setups, and most importantly: this is not intended.
602
+                return false;
603
+            }
604
+        } else {
605
+            $intName = $ldapName;
606
+        }
607
+
608
+        //a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
609
+        //disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
610
+        //NOTE: mind, disabling cache affects only this instance! Using it
611
+        // outside of core user management will still cache the user as non-existing.
612
+        $originalTTL = $this->connection->ldapCacheTTL;
613
+        $this->connection->setConfiguration(array('ldapCacheTTL' => 0));
614
+        if(($isUser && $intName !== '' && !$this->ncUserManager->userExists($intName))
615
+            || (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))) {
616
+            if($mapper->map($fdn, $intName, $uuid)) {
617
+                $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
618
+                if($this->ncUserManager instanceof PublicEmitter) {
619
+                    $this->ncUserManager->emit('\OC\User', 'assignedUserId', [$intName]);
620
+                }
621
+                $newlyMapped = true;
622
+                return $intName;
623
+            }
624
+        }
625
+        $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
626
+
627
+        $altName = $this->createAltInternalOwnCloudName($intName, $isUser);
628
+        if(is_string($altName) && $mapper->map($fdn, $altName, $uuid)) {
629
+            if($this->ncUserManager instanceof PublicEmitter) {
630
+                $this->ncUserManager->emit('\OC\User', 'assignedUserId', [$intName]);
631
+            }
632
+            $newlyMapped = true;
633
+            return $altName;
634
+        }
635
+
636
+        //if everything else did not help..
637
+        \OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', ILogger::INFO);
638
+        return false;
639
+    }
640
+
641
+    /**
642
+     * gives back the user names as they are used ownClod internally
643
+     * @param array $ldapUsers as returned by fetchList()
644
+     * @return array an array with the user names to use in Nextcloud
645
+     *
646
+     * gives back the user names as they are used ownClod internally
647
+     */
648
+    public function nextcloudUserNames($ldapUsers) {
649
+        return $this->ldap2NextcloudNames($ldapUsers, true);
650
+    }
651
+
652
+    /**
653
+     * gives back the group names as they are used ownClod internally
654
+     * @param array $ldapGroups as returned by fetchList()
655
+     * @return array an array with the group names to use in Nextcloud
656
+     *
657
+     * gives back the group names as they are used ownClod internally
658
+     */
659
+    public function nextcloudGroupNames($ldapGroups) {
660
+        return $this->ldap2NextcloudNames($ldapGroups, false);
661
+    }
662
+
663
+    /**
664
+     * @param array $ldapObjects as returned by fetchList()
665
+     * @param bool $isUsers
666
+     * @return array
667
+     */
668
+    private function ldap2NextcloudNames($ldapObjects, $isUsers) {
669
+        if($isUsers) {
670
+            $nameAttribute = $this->connection->ldapUserDisplayName;
671
+            $sndAttribute  = $this->connection->ldapUserDisplayName2;
672
+        } else {
673
+            $nameAttribute = $this->connection->ldapGroupDisplayName;
674
+        }
675
+        $nextcloudNames = array();
676
+
677
+        foreach($ldapObjects as $ldapObject) {
678
+            $nameByLDAP = null;
679
+            if(    isset($ldapObject[$nameAttribute])
680
+                && is_array($ldapObject[$nameAttribute])
681
+                && isset($ldapObject[$nameAttribute][0])
682
+            ) {
683
+                // might be set, but not necessarily. if so, we use it.
684
+                $nameByLDAP = $ldapObject[$nameAttribute][0];
685
+            }
686
+
687
+            $ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
688
+            if($ncName) {
689
+                $nextcloudNames[] = $ncName;
690
+                if($isUsers) {
691
+                    //cache the user names so it does not need to be retrieved
692
+                    //again later (e.g. sharing dialogue).
693
+                    if(is_null($nameByLDAP)) {
694
+                        continue;
695
+                    }
696
+                    $sndName = isset($ldapObject[$sndAttribute][0])
697
+                        ? $ldapObject[$sndAttribute][0] : '';
698
+                    $this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName);
699
+                }
700
+            }
701
+        }
702
+        return $nextcloudNames;
703
+    }
704
+
705
+    /**
706
+     * caches the user display name
707
+     * @param string $ocName the internal Nextcloud username
708
+     * @param string|false $home the home directory path
709
+     */
710
+    public function cacheUserHome($ocName, $home) {
711
+        $cacheKey = 'getHome'.$ocName;
712
+        $this->connection->writeToCache($cacheKey, $home);
713
+    }
714
+
715
+    /**
716
+     * caches a user as existing
717
+     * @param string $ocName the internal Nextcloud username
718
+     */
719
+    public function cacheUserExists($ocName) {
720
+        $this->connection->writeToCache('userExists'.$ocName, true);
721
+    }
722
+
723
+    /**
724
+     * caches the user display name
725
+     * @param string $ocName the internal Nextcloud username
726
+     * @param string $displayName the display name
727
+     * @param string $displayName2 the second display name
728
+     */
729
+    public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
730
+        $user = $this->userManager->get($ocName);
731
+        if($user === null) {
732
+            return;
733
+        }
734
+        $displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
735
+        $cacheKeyTrunk = 'getDisplayName';
736
+        $this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
737
+    }
738
+
739
+    /**
740
+     * creates a unique name for internal Nextcloud use for users. Don't call it directly.
741
+     * @param string $name the display name of the object
742
+     * @return string|false with with the name to use in Nextcloud or false if unsuccessful
743
+     *
744
+     * Instead of using this method directly, call
745
+     * createAltInternalOwnCloudName($name, true)
746
+     */
747
+    private function _createAltInternalOwnCloudNameForUsers($name) {
748
+        $attempts = 0;
749
+        //while loop is just a precaution. If a name is not generated within
750
+        //20 attempts, something else is very wrong. Avoids infinite loop.
751
+        while($attempts < 20){
752
+            $altName = $name . '_' . rand(1000,9999);
753
+            if(!$this->ncUserManager->userExists($altName)) {
754
+                return $altName;
755
+            }
756
+            $attempts++;
757
+        }
758
+        return false;
759
+    }
760
+
761
+    /**
762
+     * creates a unique name for internal Nextcloud use for groups. Don't call it directly.
763
+     * @param string $name the display name of the object
764
+     * @return string|false with with the name to use in Nextcloud or false if unsuccessful.
765
+     *
766
+     * Instead of using this method directly, call
767
+     * createAltInternalOwnCloudName($name, false)
768
+     *
769
+     * Group names are also used as display names, so we do a sequential
770
+     * numbering, e.g. Developers_42 when there are 41 other groups called
771
+     * "Developers"
772
+     */
773
+    private function _createAltInternalOwnCloudNameForGroups($name) {
774
+        $usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
775
+        if(!$usedNames || count($usedNames) === 0) {
776
+            $lastNo = 1; //will become name_2
777
+        } else {
778
+            natsort($usedNames);
779
+            $lastName = array_pop($usedNames);
780
+            $lastNo = (int)substr($lastName, strrpos($lastName, '_') + 1);
781
+        }
782
+        $altName = $name.'_'. (string)($lastNo+1);
783
+        unset($usedNames);
784
+
785
+        $attempts = 1;
786
+        while($attempts < 21){
787
+            // Check to be really sure it is unique
788
+            // while loop is just a precaution. If a name is not generated within
789
+            // 20 attempts, something else is very wrong. Avoids infinite loop.
790
+            if(!\OC::$server->getGroupManager()->groupExists($altName)) {
791
+                return $altName;
792
+            }
793
+            $altName = $name . '_' . ($lastNo + $attempts);
794
+            $attempts++;
795
+        }
796
+        return false;
797
+    }
798
+
799
+    /**
800
+     * creates a unique name for internal Nextcloud use.
801
+     * @param string $name the display name of the object
802
+     * @param boolean $isUser whether name should be created for a user (true) or a group (false)
803
+     * @return string|false with with the name to use in Nextcloud or false if unsuccessful
804
+     */
805
+    private function createAltInternalOwnCloudName($name, $isUser) {
806
+        $originalTTL = $this->connection->ldapCacheTTL;
807
+        $this->connection->setConfiguration(array('ldapCacheTTL' => 0));
808
+        if($isUser) {
809
+            $altName = $this->_createAltInternalOwnCloudNameForUsers($name);
810
+        } else {
811
+            $altName = $this->_createAltInternalOwnCloudNameForGroups($name);
812
+        }
813
+        $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
814
+
815
+        return $altName;
816
+    }
817
+
818
+    /**
819
+     * fetches a list of users according to a provided loginName and utilizing
820
+     * the login filter.
821
+     *
822
+     * @param string $loginName
823
+     * @param array $attributes optional, list of attributes to read
824
+     * @return array
825
+     */
826
+    public function fetchUsersByLoginName($loginName, $attributes = array('dn')) {
827
+        $loginName = $this->escapeFilterPart($loginName);
828
+        $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
829
+        return $this->fetchListOfUsers($filter, $attributes);
830
+    }
831
+
832
+    /**
833
+     * counts the number of users according to a provided loginName and
834
+     * utilizing the login filter.
835
+     *
836
+     * @param string $loginName
837
+     * @return int
838
+     */
839
+    public function countUsersByLoginName($loginName) {
840
+        $loginName = $this->escapeFilterPart($loginName);
841
+        $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
842
+        return $this->countUsers($filter);
843
+    }
844
+
845
+    /**
846
+     * @param string $filter
847
+     * @param string|string[] $attr
848
+     * @param int $limit
849
+     * @param int $offset
850
+     * @param bool $forceApplyAttributes
851
+     * @return array
852
+     */
853
+    public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null, $forceApplyAttributes = false) {
854
+        $ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
855
+        $recordsToUpdate = $ldapRecords;
856
+        if(!$forceApplyAttributes) {
857
+            $isBackgroundJobModeAjax = $this->config
858
+                    ->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
859
+            $recordsToUpdate = array_filter($ldapRecords, function($record) use ($isBackgroundJobModeAjax) {
860
+                $newlyMapped = false;
861
+                $uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record);
862
+                if(is_string($uid)) {
863
+                    $this->cacheUserExists($uid);
864
+                }
865
+                return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax);
866
+            });
867
+        }
868
+        $this->batchApplyUserAttributes($recordsToUpdate);
869
+        return $this->fetchList($ldapRecords, count($attr) > 1);
870
+    }
871
+
872
+    /**
873
+     * provided with an array of LDAP user records the method will fetch the
874
+     * user object and requests it to process the freshly fetched attributes and
875
+     * and their values
876
+     * @param array $ldapRecords
877
+     */
878
+    public function batchApplyUserAttributes(array $ldapRecords){
879
+        $displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
880
+        foreach($ldapRecords as $userRecord) {
881
+            if(!isset($userRecord[$displayNameAttribute])) {
882
+                // displayName is obligatory
883
+                continue;
884
+            }
885
+            $ocName  = $this->dn2ocname($userRecord['dn'][0], null, true);
886
+            if($ocName === false) {
887
+                continue;
888
+            }
889
+            $user = $this->userManager->get($ocName);
890
+            if($user instanceof OfflineUser) {
891
+                $user->unmark();
892
+                $user = $this->userManager->get($ocName);
893
+            }
894
+            if ($user !== null) {
895
+                $user->processAttributes($userRecord);
896
+            } else {
897
+                \OC::$server->getLogger()->debug(
898
+                    "The ldap user manager returned null for $ocName",
899
+                    ['app'=>'user_ldap']
900
+                );
901
+            }
902
+        }
903
+    }
904
+
905
+    /**
906
+     * @param string $filter
907
+     * @param string|string[] $attr
908
+     * @param int $limit
909
+     * @param int $offset
910
+     * @return array
911
+     */
912
+    public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
913
+        return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), count($attr) > 1);
914
+    }
915
+
916
+    /**
917
+     * @param array $list
918
+     * @param bool $manyAttributes
919
+     * @return array
920
+     */
921
+    private function fetchList($list, $manyAttributes) {
922
+        if(is_array($list)) {
923
+            if($manyAttributes) {
924
+                return $list;
925
+            } else {
926
+                $list = array_reduce($list, function($carry, $item) {
927
+                    $attribute = array_keys($item)[0];
928
+                    $carry[] = $item[$attribute][0];
929
+                    return $carry;
930
+                }, array());
931
+                return array_unique($list, SORT_LOCALE_STRING);
932
+            }
933
+        }
934
+
935
+        //error cause actually, maybe throw an exception in future.
936
+        return array();
937
+    }
938
+
939
+    /**
940
+     * executes an LDAP search, optimized for Users
941
+     * @param string $filter the LDAP filter for the search
942
+     * @param string|string[] $attr optional, when a certain attribute shall be filtered out
943
+     * @param integer $limit
944
+     * @param integer $offset
945
+     * @return array with the search result
946
+     *
947
+     * Executes an LDAP search
948
+     */
949
+    public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
950
+        return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
951
+    }
952
+
953
+    /**
954
+     * @param string $filter
955
+     * @param string|string[] $attr
956
+     * @param int $limit
957
+     * @param int $offset
958
+     * @return false|int
959
+     */
960
+    public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
961
+        return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
962
+    }
963
+
964
+    /**
965
+     * executes an LDAP search, optimized for Groups
966
+     * @param string $filter the LDAP filter for the search
967
+     * @param string|string[] $attr optional, when a certain attribute shall be filtered out
968
+     * @param integer $limit
969
+     * @param integer $offset
970
+     * @return array with the search result
971
+     *
972
+     * Executes an LDAP search
973
+     */
974
+    public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
975
+        return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
976
+    }
977
+
978
+    /**
979
+     * returns the number of available groups
980
+     * @param string $filter the LDAP search filter
981
+     * @param string[] $attr optional
982
+     * @param int|null $limit
983
+     * @param int|null $offset
984
+     * @return int|bool
985
+     */
986
+    public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
987
+        return $this->count($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
988
+    }
989
+
990
+    /**
991
+     * returns the number of available objects on the base DN
992
+     *
993
+     * @param int|null $limit
994
+     * @param int|null $offset
995
+     * @return int|bool
996
+     */
997
+    public function countObjects($limit = null, $offset = null) {
998
+        return $this->count('objectclass=*', $this->connection->ldapBase, array('dn'), $limit, $offset);
999
+    }
1000
+
1001
+    /**
1002
+     * Returns the LDAP handler
1003
+     * @throws \OC\ServerNotAvailableException
1004
+     */
1005
+
1006
+    /**
1007
+     * @return mixed
1008
+     * @throws \OC\ServerNotAvailableException
1009
+     */
1010
+    private function invokeLDAPMethod() {
1011
+        $arguments = func_get_args();
1012
+        $command = array_shift($arguments);
1013
+        $cr = array_shift($arguments);
1014
+        if (!method_exists($this->ldap, $command)) {
1015
+            return null;
1016
+        }
1017
+        array_unshift($arguments, $cr);
1018
+        // php no longer supports call-time pass-by-reference
1019
+        // thus cannot support controlPagedResultResponse as the third argument
1020
+        // is a reference
1021
+        $doMethod = function () use ($command, &$arguments) {
1022
+            if ($command == 'controlPagedResultResponse') {
1023
+                throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
1024
+            } else {
1025
+                return call_user_func_array(array($this->ldap, $command), $arguments);
1026
+            }
1027
+        };
1028
+        try {
1029
+            $ret = $doMethod();
1030
+        } catch (ServerNotAvailableException $e) {
1031
+            /* Server connection lost, attempt to reestablish it
1032 1032
 			 * Maybe implement exponential backoff?
1033 1033
 			 * This was enough to get solr indexer working which has large delays between LDAP fetches.
1034 1034
 			 */
1035
-			\OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", ILogger::DEBUG);
1036
-			$this->connection->resetConnectionResource();
1037
-			$cr = $this->connection->getConnectionResource();
1038
-
1039
-			if(!$this->ldap->isResource($cr)) {
1040
-				// Seems like we didn't find any resource.
1041
-				\OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", ILogger::DEBUG);
1042
-				throw $e;
1043
-			}
1044
-
1045
-			$arguments[0] = array_pad([], count($arguments[0]), $cr);
1046
-			$ret = $doMethod();
1047
-		}
1048
-		return $ret;
1049
-	}
1050
-
1051
-	/**
1052
-	 * retrieved. Results will according to the order in the array.
1053
-	 *
1054
-	 * @param $filter
1055
-	 * @param $base
1056
-	 * @param string[]|string|null $attr
1057
-	 * @param int $limit optional, maximum results to be counted
1058
-	 * @param int $offset optional, a starting point
1059
-	 * @return array|false array with the search result as first value and pagedSearchOK as
1060
-	 * second | false if not successful
1061
-	 * @throws ServerNotAvailableException
1062
-	 */
1063
-	private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
1064
-		if(!is_null($attr) && !is_array($attr)) {
1065
-			$attr = array(mb_strtolower($attr, 'UTF-8'));
1066
-		}
1067
-
1068
-		// See if we have a resource, in case not cancel with message
1069
-		$cr = $this->connection->getConnectionResource();
1070
-		if(!$this->ldap->isResource($cr)) {
1071
-			// Seems like we didn't find any resource.
1072
-			// Return an empty array just like before.
1073
-			\OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', ILogger::DEBUG);
1074
-			return false;
1075
-		}
1076
-
1077
-		//check whether paged search should be attempted
1078
-		$pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, $offset);
1079
-
1080
-		$linkResources = array_pad(array(), count($base), $cr);
1081
-		$sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr);
1082
-		// cannot use $cr anymore, might have changed in the previous call!
1083
-		$error = $this->ldap->errno($this->connection->getConnectionResource());
1084
-		if(!is_array($sr) || $error !== 0) {
1085
-			\OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), ILogger::ERROR);
1086
-			return false;
1087
-		}
1088
-
1089
-		return array($sr, $pagedSearchOK);
1090
-	}
1091
-
1092
-	/**
1093
-	 * processes an LDAP paged search operation
1094
-	 * @param array $sr the array containing the LDAP search resources
1095
-	 * @param string $filter the LDAP filter for the search
1096
-	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1097
-	 * @param int $iFoundItems number of results in the single search operation
1098
-	 * @param int $limit maximum results to be counted
1099
-	 * @param int $offset a starting point
1100
-	 * @param bool $pagedSearchOK whether a paged search has been executed
1101
-	 * @param bool $skipHandling required for paged search when cookies to
1102
-	 * prior results need to be gained
1103
-	 * @return bool cookie validity, true if we have more pages, false otherwise.
1104
-	 */
1105
-	private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
1106
-		$cookie = null;
1107
-		if($pagedSearchOK) {
1108
-			$cr = $this->connection->getConnectionResource();
1109
-			foreach($sr as $key => $res) {
1110
-				if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
1111
-					$this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
1112
-				}
1113
-			}
1114
-
1115
-			//browsing through prior pages to get the cookie for the new one
1116
-			if($skipHandling) {
1117
-				return false;
1118
-			}
1119
-			// if count is bigger, then the server does not support
1120
-			// paged search. Instead, he did a normal search. We set a
1121
-			// flag here, so the callee knows how to deal with it.
1122
-			if($iFoundItems <= $limit) {
1123
-				$this->pagedSearchedSuccessful = true;
1124
-			}
1125
-		} else {
1126
-			if(!is_null($limit) && (int)$this->connection->ldapPagingSize !== 0) {
1127
-				\OC::$server->getLogger()->debug(
1128
-					'Paged search was not available',
1129
-					[ 'app' => 'user_ldap' ]
1130
-				);
1131
-			}
1132
-		}
1133
-		/* ++ Fixing RHDS searches with pages with zero results ++
1035
+            \OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", ILogger::DEBUG);
1036
+            $this->connection->resetConnectionResource();
1037
+            $cr = $this->connection->getConnectionResource();
1038
+
1039
+            if(!$this->ldap->isResource($cr)) {
1040
+                // Seems like we didn't find any resource.
1041
+                \OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", ILogger::DEBUG);
1042
+                throw $e;
1043
+            }
1044
+
1045
+            $arguments[0] = array_pad([], count($arguments[0]), $cr);
1046
+            $ret = $doMethod();
1047
+        }
1048
+        return $ret;
1049
+    }
1050
+
1051
+    /**
1052
+     * retrieved. Results will according to the order in the array.
1053
+     *
1054
+     * @param $filter
1055
+     * @param $base
1056
+     * @param string[]|string|null $attr
1057
+     * @param int $limit optional, maximum results to be counted
1058
+     * @param int $offset optional, a starting point
1059
+     * @return array|false array with the search result as first value and pagedSearchOK as
1060
+     * second | false if not successful
1061
+     * @throws ServerNotAvailableException
1062
+     */
1063
+    private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
1064
+        if(!is_null($attr) && !is_array($attr)) {
1065
+            $attr = array(mb_strtolower($attr, 'UTF-8'));
1066
+        }
1067
+
1068
+        // See if we have a resource, in case not cancel with message
1069
+        $cr = $this->connection->getConnectionResource();
1070
+        if(!$this->ldap->isResource($cr)) {
1071
+            // Seems like we didn't find any resource.
1072
+            // Return an empty array just like before.
1073
+            \OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', ILogger::DEBUG);
1074
+            return false;
1075
+        }
1076
+
1077
+        //check whether paged search should be attempted
1078
+        $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, $offset);
1079
+
1080
+        $linkResources = array_pad(array(), count($base), $cr);
1081
+        $sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr);
1082
+        // cannot use $cr anymore, might have changed in the previous call!
1083
+        $error = $this->ldap->errno($this->connection->getConnectionResource());
1084
+        if(!is_array($sr) || $error !== 0) {
1085
+            \OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), ILogger::ERROR);
1086
+            return false;
1087
+        }
1088
+
1089
+        return array($sr, $pagedSearchOK);
1090
+    }
1091
+
1092
+    /**
1093
+     * processes an LDAP paged search operation
1094
+     * @param array $sr the array containing the LDAP search resources
1095
+     * @param string $filter the LDAP filter for the search
1096
+     * @param array $base an array containing the LDAP subtree(s) that shall be searched
1097
+     * @param int $iFoundItems number of results in the single search operation
1098
+     * @param int $limit maximum results to be counted
1099
+     * @param int $offset a starting point
1100
+     * @param bool $pagedSearchOK whether a paged search has been executed
1101
+     * @param bool $skipHandling required for paged search when cookies to
1102
+     * prior results need to be gained
1103
+     * @return bool cookie validity, true if we have more pages, false otherwise.
1104
+     */
1105
+    private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
1106
+        $cookie = null;
1107
+        if($pagedSearchOK) {
1108
+            $cr = $this->connection->getConnectionResource();
1109
+            foreach($sr as $key => $res) {
1110
+                if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
1111
+                    $this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
1112
+                }
1113
+            }
1114
+
1115
+            //browsing through prior pages to get the cookie for the new one
1116
+            if($skipHandling) {
1117
+                return false;
1118
+            }
1119
+            // if count is bigger, then the server does not support
1120
+            // paged search. Instead, he did a normal search. We set a
1121
+            // flag here, so the callee knows how to deal with it.
1122
+            if($iFoundItems <= $limit) {
1123
+                $this->pagedSearchedSuccessful = true;
1124
+            }
1125
+        } else {
1126
+            if(!is_null($limit) && (int)$this->connection->ldapPagingSize !== 0) {
1127
+                \OC::$server->getLogger()->debug(
1128
+                    'Paged search was not available',
1129
+                    [ 'app' => 'user_ldap' ]
1130
+                );
1131
+            }
1132
+        }
1133
+        /* ++ Fixing RHDS searches with pages with zero results ++
1134 1134
 		 * Return cookie status. If we don't have more pages, with RHDS
1135 1135
 		 * cookie is null, with openldap cookie is an empty string and
1136 1136
 		 * to 386ds '0' is a valid cookie. Even if $iFoundItems == 0
1137 1137
 		 */
1138
-		return !empty($cookie) || $cookie === '0';
1139
-	}
1140
-
1141
-	/**
1142
-	 * executes an LDAP search, but counts the results only
1143
-	 *
1144
-	 * @param string $filter the LDAP filter for the search
1145
-	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1146
-	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1147
-	 * retrieved. Results will according to the order in the array.
1148
-	 * @param int $limit optional, maximum results to be counted
1149
-	 * @param int $offset optional, a starting point
1150
-	 * @param bool $skipHandling indicates whether the pages search operation is
1151
-	 * completed
1152
-	 * @return int|false Integer or false if the search could not be initialized
1153
-	 * @throws ServerNotAvailableException
1154
-	 */
1155
-	private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1156
-		\OCP\Util::writeLog('user_ldap', 'Count filter:  '.print_r($filter, true), ILogger::DEBUG);
1157
-
1158
-		$limitPerPage = (int)$this->connection->ldapPagingSize;
1159
-		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1160
-			$limitPerPage = $limit;
1161
-		}
1162
-
1163
-		$counter = 0;
1164
-		$count = null;
1165
-		$this->connection->getConnectionResource();
1166
-
1167
-		do {
1168
-			$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1169
-			if($search === false) {
1170
-				return $counter > 0 ? $counter : false;
1171
-			}
1172
-			list($sr, $pagedSearchOK) = $search;
1173
-
1174
-			/* ++ Fixing RHDS searches with pages with zero results ++
1138
+        return !empty($cookie) || $cookie === '0';
1139
+    }
1140
+
1141
+    /**
1142
+     * executes an LDAP search, but counts the results only
1143
+     *
1144
+     * @param string $filter the LDAP filter for the search
1145
+     * @param array $base an array containing the LDAP subtree(s) that shall be searched
1146
+     * @param string|string[] $attr optional, array, one or more attributes that shall be
1147
+     * retrieved. Results will according to the order in the array.
1148
+     * @param int $limit optional, maximum results to be counted
1149
+     * @param int $offset optional, a starting point
1150
+     * @param bool $skipHandling indicates whether the pages search operation is
1151
+     * completed
1152
+     * @return int|false Integer or false if the search could not be initialized
1153
+     * @throws ServerNotAvailableException
1154
+     */
1155
+    private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1156
+        \OCP\Util::writeLog('user_ldap', 'Count filter:  '.print_r($filter, true), ILogger::DEBUG);
1157
+
1158
+        $limitPerPage = (int)$this->connection->ldapPagingSize;
1159
+        if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1160
+            $limitPerPage = $limit;
1161
+        }
1162
+
1163
+        $counter = 0;
1164
+        $count = null;
1165
+        $this->connection->getConnectionResource();
1166
+
1167
+        do {
1168
+            $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1169
+            if($search === false) {
1170
+                return $counter > 0 ? $counter : false;
1171
+            }
1172
+            list($sr, $pagedSearchOK) = $search;
1173
+
1174
+            /* ++ Fixing RHDS searches with pages with zero results ++
1175 1175
 			 * countEntriesInSearchResults() method signature changed
1176 1176
 			 * by removing $limit and &$hasHitLimit parameters
1177 1177
 			 */
1178
-			$count = $this->countEntriesInSearchResults($sr);
1179
-			$counter += $count;
1178
+            $count = $this->countEntriesInSearchResults($sr);
1179
+            $counter += $count;
1180 1180
 
1181
-			$hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
1182
-										$offset, $pagedSearchOK, $skipHandling);
1183
-			$offset += $limitPerPage;
1184
-			/* ++ Fixing RHDS searches with pages with zero results ++
1181
+            $hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
1182
+                                        $offset, $pagedSearchOK, $skipHandling);
1183
+            $offset += $limitPerPage;
1184
+            /* ++ Fixing RHDS searches with pages with zero results ++
1185 1185
 			 * Continue now depends on $hasMorePages value
1186 1186
 			 */
1187
-			$continue = $pagedSearchOK && $hasMorePages;
1188
-		} while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
1189
-
1190
-		return $counter;
1191
-	}
1192
-
1193
-	/**
1194
-	 * @param array $searchResults
1195
-	 * @return int
1196
-	 */
1197
-	private function countEntriesInSearchResults($searchResults) {
1198
-		$counter = 0;
1199
-
1200
-		foreach($searchResults as $res) {
1201
-			$count = (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $res);
1202
-			$counter += $count;
1203
-		}
1204
-
1205
-		return $counter;
1206
-	}
1207
-
1208
-	/**
1209
-	 * Executes an LDAP search
1210
-	 *
1211
-	 * @param string $filter the LDAP filter for the search
1212
-	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1213
-	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1214
-	 * @param int $limit
1215
-	 * @param int $offset
1216
-	 * @param bool $skipHandling
1217
-	 * @return array with the search result
1218
-	 * @throws ServerNotAvailableException
1219
-	 */
1220
-	public function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1221
-		$limitPerPage = (int)$this->connection->ldapPagingSize;
1222
-		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1223
-			$limitPerPage = $limit;
1224
-		}
1225
-
1226
-		/* ++ Fixing RHDS searches with pages with zero results ++
1187
+            $continue = $pagedSearchOK && $hasMorePages;
1188
+        } while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
1189
+
1190
+        return $counter;
1191
+    }
1192
+
1193
+    /**
1194
+     * @param array $searchResults
1195
+     * @return int
1196
+     */
1197
+    private function countEntriesInSearchResults($searchResults) {
1198
+        $counter = 0;
1199
+
1200
+        foreach($searchResults as $res) {
1201
+            $count = (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $res);
1202
+            $counter += $count;
1203
+        }
1204
+
1205
+        return $counter;
1206
+    }
1207
+
1208
+    /**
1209
+     * Executes an LDAP search
1210
+     *
1211
+     * @param string $filter the LDAP filter for the search
1212
+     * @param array $base an array containing the LDAP subtree(s) that shall be searched
1213
+     * @param string|string[] $attr optional, array, one or more attributes that shall be
1214
+     * @param int $limit
1215
+     * @param int $offset
1216
+     * @param bool $skipHandling
1217
+     * @return array with the search result
1218
+     * @throws ServerNotAvailableException
1219
+     */
1220
+    public function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1221
+        $limitPerPage = (int)$this->connection->ldapPagingSize;
1222
+        if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1223
+            $limitPerPage = $limit;
1224
+        }
1225
+
1226
+        /* ++ Fixing RHDS searches with pages with zero results ++
1227 1227
 		 * As we can have pages with zero results and/or pages with less
1228 1228
 		 * than $limit results but with a still valid server 'cookie',
1229 1229
 		 * loops through until we get $continue equals true and
1230 1230
 		 * $findings['count'] < $limit
1231 1231
 		 */
1232
-		$findings = [];
1233
-		$savedoffset = $offset;
1234
-		do {
1235
-			$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1236
-			if($search === false) {
1237
-				return [];
1238
-			}
1239
-			list($sr, $pagedSearchOK) = $search;
1240
-			$cr = $this->connection->getConnectionResource();
1241
-
1242
-			if($skipHandling) {
1243
-				//i.e. result do not need to be fetched, we just need the cookie
1244
-				//thus pass 1 or any other value as $iFoundItems because it is not
1245
-				//used
1246
-				$this->processPagedSearchStatus($sr, $filter, $base, 1, $limitPerPage,
1247
-								$offset, $pagedSearchOK,
1248
-								$skipHandling);
1249
-				return array();
1250
-			}
1251
-
1252
-			$iFoundItems = 0;
1253
-			foreach($sr as $res) {
1254
-				$findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $res));
1255
-				$iFoundItems = max($iFoundItems, $findings['count']);
1256
-				unset($findings['count']);
1257
-			}
1258
-
1259
-			$continue = $this->processPagedSearchStatus($sr, $filter, $base, $iFoundItems,
1260
-				$limitPerPage, $offset, $pagedSearchOK,
1261
-										$skipHandling);
1262
-			$offset += $limitPerPage;
1263
-		} while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit));
1264
-		// reseting offset
1265
-		$offset = $savedoffset;
1266
-
1267
-		// if we're here, probably no connection resource is returned.
1268
-		// to make Nextcloud behave nicely, we simply give back an empty array.
1269
-		if(is_null($findings)) {
1270
-			return array();
1271
-		}
1272
-
1273
-		if(!is_null($attr)) {
1274
-			$selection = [];
1275
-			$i = 0;
1276
-			foreach($findings as $item) {
1277
-				if(!is_array($item)) {
1278
-					continue;
1279
-				}
1280
-				$item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1281
-				foreach($attr as $key) {
1282
-					if(isset($item[$key])) {
1283
-						if(is_array($item[$key]) && isset($item[$key]['count'])) {
1284
-							unset($item[$key]['count']);
1285
-						}
1286
-						if($key !== 'dn') {
1287
-							if($this->resemblesDN($key)) {
1288
-								$selection[$i][$key] = $this->helper->sanitizeDN($item[$key]);
1289
-							} else if($key === 'objectguid' || $key === 'guid') {
1290
-								$selection[$i][$key] = [$this->convertObjectGUID2Str($item[$key][0])];
1291
-							} else {
1292
-								$selection[$i][$key] = $item[$key];
1293
-							}
1294
-						} else {
1295
-							$selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
1296
-						}
1297
-					}
1298
-
1299
-				}
1300
-				$i++;
1301
-			}
1302
-			$findings = $selection;
1303
-		}
1304
-		//we slice the findings, when
1305
-		//a) paged search unsuccessful, though attempted
1306
-		//b) no paged search, but limit set
1307
-		if((!$this->getPagedSearchResultState()
1308
-			&& $pagedSearchOK)
1309
-			|| (
1310
-				!$pagedSearchOK
1311
-				&& !is_null($limit)
1312
-			)
1313
-		) {
1314
-			$findings = array_slice($findings, (int)$offset, $limit);
1315
-		}
1316
-		return $findings;
1317
-	}
1318
-
1319
-	/**
1320
-	 * @param string $name
1321
-	 * @return string
1322
-	 * @throws \InvalidArgumentException
1323
-	 */
1324
-	public function sanitizeUsername($name) {
1325
-		$name = trim($name);
1326
-
1327
-		if($this->connection->ldapIgnoreNamingRules) {
1328
-			return $name;
1329
-		}
1330
-
1331
-		// Transliteration to ASCII
1332
-		$transliterated = @iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1333
-		if($transliterated !== false) {
1334
-			// depending on system config iconv can work or not
1335
-			$name = $transliterated;
1336
-		}
1337
-
1338
-		// Replacements
1339
-		$name = str_replace(' ', '_', $name);
1340
-
1341
-		// Every remaining disallowed characters will be removed
1342
-		$name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1343
-
1344
-		if($name === '') {
1345
-			throw new \InvalidArgumentException('provided name template for username does not contain any allowed characters');
1346
-		}
1347
-
1348
-		return $name;
1349
-	}
1350
-
1351
-	/**
1352
-	* escapes (user provided) parts for LDAP filter
1353
-	* @param string $input, the provided value
1354
-	* @param bool $allowAsterisk whether in * at the beginning should be preserved
1355
-	* @return string the escaped string
1356
-	*/
1357
-	public function escapeFilterPart($input, $allowAsterisk = false) {
1358
-		$asterisk = '';
1359
-		if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1360
-			$asterisk = '*';
1361
-			$input = mb_substr($input, 1, null, 'UTF-8');
1362
-		}
1363
-		$search  = array('*', '\\', '(', ')');
1364
-		$replace = array('\\*', '\\\\', '\\(', '\\)');
1365
-		return $asterisk . str_replace($search, $replace, $input);
1366
-	}
1367
-
1368
-	/**
1369
-	 * combines the input filters with AND
1370
-	 * @param string[] $filters the filters to connect
1371
-	 * @return string the combined filter
1372
-	 */
1373
-	public function combineFilterWithAnd($filters) {
1374
-		return $this->combineFilter($filters, '&');
1375
-	}
1376
-
1377
-	/**
1378
-	 * combines the input filters with OR
1379
-	 * @param string[] $filters the filters to connect
1380
-	 * @return string the combined filter
1381
-	 * Combines Filter arguments with OR
1382
-	 */
1383
-	public function combineFilterWithOr($filters) {
1384
-		return $this->combineFilter($filters, '|');
1385
-	}
1386
-
1387
-	/**
1388
-	 * combines the input filters with given operator
1389
-	 * @param string[] $filters the filters to connect
1390
-	 * @param string $operator either & or |
1391
-	 * @return string the combined filter
1392
-	 */
1393
-	private function combineFilter($filters, $operator) {
1394
-		$combinedFilter = '('.$operator;
1395
-		foreach($filters as $filter) {
1396
-			if ($filter !== '' && $filter[0] !== '(') {
1397
-				$filter = '('.$filter.')';
1398
-			}
1399
-			$combinedFilter.=$filter;
1400
-		}
1401
-		$combinedFilter.=')';
1402
-		return $combinedFilter;
1403
-	}
1404
-
1405
-	/**
1406
-	 * creates a filter part for to perform search for users
1407
-	 * @param string $search the search term
1408
-	 * @return string the final filter part to use in LDAP searches
1409
-	 */
1410
-	public function getFilterPartForUserSearch($search) {
1411
-		return $this->getFilterPartForSearch($search,
1412
-			$this->connection->ldapAttributesForUserSearch,
1413
-			$this->connection->ldapUserDisplayName);
1414
-	}
1415
-
1416
-	/**
1417
-	 * creates a filter part for to perform search for groups
1418
-	 * @param string $search the search term
1419
-	 * @return string the final filter part to use in LDAP searches
1420
-	 */
1421
-	public function getFilterPartForGroupSearch($search) {
1422
-		return $this->getFilterPartForSearch($search,
1423
-			$this->connection->ldapAttributesForGroupSearch,
1424
-			$this->connection->ldapGroupDisplayName);
1425
-	}
1426
-
1427
-	/**
1428
-	 * creates a filter part for searches by splitting up the given search
1429
-	 * string into single words
1430
-	 * @param string $search the search term
1431
-	 * @param string[] $searchAttributes needs to have at least two attributes,
1432
-	 * otherwise it does not make sense :)
1433
-	 * @return string the final filter part to use in LDAP searches
1434
-	 * @throws \Exception
1435
-	 */
1436
-	private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1437
-		if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
1438
-			throw new \Exception('searchAttributes must be an array with at least two string');
1439
-		}
1440
-		$searchWords = explode(' ', trim($search));
1441
-		$wordFilters = array();
1442
-		foreach($searchWords as $word) {
1443
-			$word = $this->prepareSearchTerm($word);
1444
-			//every word needs to appear at least once
1445
-			$wordMatchOneAttrFilters = array();
1446
-			foreach($searchAttributes as $attr) {
1447
-				$wordMatchOneAttrFilters[] = $attr . '=' . $word;
1448
-			}
1449
-			$wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1450
-		}
1451
-		return $this->combineFilterWithAnd($wordFilters);
1452
-	}
1453
-
1454
-	/**
1455
-	 * creates a filter part for searches
1456
-	 * @param string $search the search term
1457
-	 * @param string[]|null $searchAttributes
1458
-	 * @param string $fallbackAttribute a fallback attribute in case the user
1459
-	 * did not define search attributes. Typically the display name attribute.
1460
-	 * @return string the final filter part to use in LDAP searches
1461
-	 */
1462
-	private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1463
-		$filter = array();
1464
-		$haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1465
-		if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1466
-			try {
1467
-				return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1468
-			} catch(\Exception $e) {
1469
-				\OCP\Util::writeLog(
1470
-					'user_ldap',
1471
-					'Creating advanced filter for search failed, falling back to simple method.',
1472
-					ILogger::INFO
1473
-				);
1474
-			}
1475
-		}
1476
-
1477
-		$search = $this->prepareSearchTerm($search);
1478
-		if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
1479
-			if ($fallbackAttribute === '') {
1480
-				return '';
1481
-			}
1482
-			$filter[] = $fallbackAttribute . '=' . $search;
1483
-		} else {
1484
-			foreach($searchAttributes as $attribute) {
1485
-				$filter[] = $attribute . '=' . $search;
1486
-			}
1487
-		}
1488
-		if(count($filter) === 1) {
1489
-			return '('.$filter[0].')';
1490
-		}
1491
-		return $this->combineFilterWithOr($filter);
1492
-	}
1493
-
1494
-	/**
1495
-	 * returns the search term depending on whether we are allowed
1496
-	 * list users found by ldap with the current input appended by
1497
-	 * a *
1498
-	 * @return string
1499
-	 */
1500
-	private function prepareSearchTerm($term) {
1501
-		$config = \OC::$server->getConfig();
1502
-
1503
-		$allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
1504
-
1505
-		$result = $term;
1506
-		if ($term === '') {
1507
-			$result = '*';
1508
-		} else if ($allowEnum !== 'no') {
1509
-			$result = $term . '*';
1510
-		}
1511
-		return $result;
1512
-	}
1513
-
1514
-	/**
1515
-	 * returns the filter used for counting users
1516
-	 * @return string
1517
-	 */
1518
-	public function getFilterForUserCount() {
1519
-		$filter = $this->combineFilterWithAnd(array(
1520
-			$this->connection->ldapUserFilter,
1521
-			$this->connection->ldapUserDisplayName . '=*'
1522
-		));
1523
-
1524
-		return $filter;
1525
-	}
1526
-
1527
-	/**
1528
-	 * @param string $name
1529
-	 * @param string $password
1530
-	 * @return bool
1531
-	 */
1532
-	public function areCredentialsValid($name, $password) {
1533
-		$name = $this->helper->DNasBaseParameter($name);
1534
-		$testConnection = clone $this->connection;
1535
-		$credentials = array(
1536
-			'ldapAgentName' => $name,
1537
-			'ldapAgentPassword' => $password
1538
-		);
1539
-		if(!$testConnection->setConfiguration($credentials)) {
1540
-			return false;
1541
-		}
1542
-		return $testConnection->bind();
1543
-	}
1544
-
1545
-	/**
1546
-	 * reverse lookup of a DN given a known UUID
1547
-	 *
1548
-	 * @param string $uuid
1549
-	 * @return string
1550
-	 * @throws \Exception
1551
-	 */
1552
-	public function getUserDnByUuid($uuid) {
1553
-		$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1554
-		$filter       = $this->connection->ldapUserFilter;
1555
-		$base         = $this->connection->ldapBaseUsers;
1556
-
1557
-		if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
1558
-			// Sacrebleu! The UUID attribute is unknown :( We need first an
1559
-			// existing DN to be able to reliably detect it.
1560
-			$result = $this->search($filter, $base, ['dn'], 1);
1561
-			if(!isset($result[0]) || !isset($result[0]['dn'])) {
1562
-				throw new \Exception('Cannot determine UUID attribute');
1563
-			}
1564
-			$dn = $result[0]['dn'][0];
1565
-			if(!$this->detectUuidAttribute($dn, true)) {
1566
-				throw new \Exception('Cannot determine UUID attribute');
1567
-			}
1568
-		} else {
1569
-			// The UUID attribute is either known or an override is given.
1570
-			// By calling this method we ensure that $this->connection->$uuidAttr
1571
-			// is definitely set
1572
-			if(!$this->detectUuidAttribute('', true)) {
1573
-				throw new \Exception('Cannot determine UUID attribute');
1574
-			}
1575
-		}
1576
-
1577
-		$uuidAttr = $this->connection->ldapUuidUserAttribute;
1578
-		if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1579
-			$uuid = $this->formatGuid2ForFilterUser($uuid);
1580
-		}
1581
-
1582
-		$filter = $uuidAttr . '=' . $uuid;
1583
-		$result = $this->searchUsers($filter, ['dn'], 2);
1584
-		if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1585
-			// we put the count into account to make sure that this is
1586
-			// really unique
1587
-			return $result[0]['dn'][0];
1588
-		}
1589
-
1590
-		throw new \Exception('Cannot determine UUID attribute');
1591
-	}
1592
-
1593
-	/**
1594
-	 * auto-detects the directory's UUID attribute
1595
-	 *
1596
-	 * @param string $dn a known DN used to check against
1597
-	 * @param bool $isUser
1598
-	 * @param bool $force the detection should be run, even if it is not set to auto
1599
-	 * @param array|null $ldapRecord
1600
-	 * @return bool true on success, false otherwise
1601
-	 */
1602
-	private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) {
1603
-		if($isUser) {
1604
-			$uuidAttr     = 'ldapUuidUserAttribute';
1605
-			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1606
-		} else {
1607
-			$uuidAttr     = 'ldapUuidGroupAttribute';
1608
-			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1609
-		}
1610
-
1611
-		if(($this->connection->$uuidAttr !== 'auto') && !$force) {
1612
-			return true;
1613
-		}
1614
-
1615
-		if (is_string($uuidOverride) && trim($uuidOverride) !== '' && !$force) {
1616
-			$this->connection->$uuidAttr = $uuidOverride;
1617
-			return true;
1618
-		}
1619
-
1620
-		foreach(self::UUID_ATTRIBUTES as $attribute) {
1621
-			if($ldapRecord !== null) {
1622
-				// we have the info from LDAP already, we don't need to talk to the server again
1623
-				if(isset($ldapRecord[$attribute])) {
1624
-					$this->connection->$uuidAttr = $attribute;
1625
-					return true;
1626
-				} else {
1627
-					continue;
1628
-				}
1629
-			}
1630
-
1631
-			$value = $this->readAttribute($dn, $attribute);
1632
-			if(is_array($value) && isset($value[0]) && !empty($value[0])) {
1633
-				\OCP\Util::writeLog(
1634
-					'user_ldap',
1635
-					'Setting '.$attribute.' as '.$uuidAttr,
1636
-					ILogger::DEBUG
1637
-				);
1638
-				$this->connection->$uuidAttr = $attribute;
1639
-				return true;
1640
-			}
1641
-		}
1642
-		\OCP\Util::writeLog(
1643
-			'user_ldap',
1644
-			'Could not autodetect the UUID attribute',
1645
-			ILogger::ERROR
1646
-		);
1647
-
1648
-		return false;
1649
-	}
1650
-
1651
-	/**
1652
-	 * @param string $dn
1653
-	 * @param bool $isUser
1654
-	 * @param null $ldapRecord
1655
-	 * @return bool|string
1656
-	 */
1657
-	public function getUUID($dn, $isUser = true, $ldapRecord = null) {
1658
-		if($isUser) {
1659
-			$uuidAttr     = 'ldapUuidUserAttribute';
1660
-			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1661
-		} else {
1662
-			$uuidAttr     = 'ldapUuidGroupAttribute';
1663
-			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1664
-		}
1665
-
1666
-		$uuid = false;
1667
-		if($this->detectUuidAttribute($dn, $isUser, false, $ldapRecord)) {
1668
-			$attr = $this->connection->$uuidAttr;
1669
-			$uuid = isset($ldapRecord[$attr]) ? $ldapRecord[$attr] : $this->readAttribute($dn, $attr);
1670
-			if( !is_array($uuid)
1671
-				&& $uuidOverride !== ''
1672
-				&& $this->detectUuidAttribute($dn, $isUser, true, $ldapRecord))
1673
-			{
1674
-				$uuid = isset($ldapRecord[$this->connection->$uuidAttr])
1675
-					? $ldapRecord[$this->connection->$uuidAttr]
1676
-					: $this->readAttribute($dn, $this->connection->$uuidAttr);
1677
-			}
1678
-			if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
1679
-				$uuid = $uuid[0];
1680
-			}
1681
-		}
1682
-
1683
-		return $uuid;
1684
-	}
1685
-
1686
-	/**
1687
-	 * converts a binary ObjectGUID into a string representation
1688
-	 * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
1689
-	 * @return string
1690
-	 * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
1691
-	 */
1692
-	private function convertObjectGUID2Str($oguid) {
1693
-		$hex_guid = bin2hex($oguid);
1694
-		$hex_guid_to_guid_str = '';
1695
-		for($k = 1; $k <= 4; ++$k) {
1696
-			$hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
1697
-		}
1698
-		$hex_guid_to_guid_str .= '-';
1699
-		for($k = 1; $k <= 2; ++$k) {
1700
-			$hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
1701
-		}
1702
-		$hex_guid_to_guid_str .= '-';
1703
-		for($k = 1; $k <= 2; ++$k) {
1704
-			$hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
1705
-		}
1706
-		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
1707
-		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
1708
-
1709
-		return strtoupper($hex_guid_to_guid_str);
1710
-	}
1711
-
1712
-	/**
1713
-	 * the first three blocks of the string-converted GUID happen to be in
1714
-	 * reverse order. In order to use it in a filter, this needs to be
1715
-	 * corrected. Furthermore the dashes need to be replaced and \\ preprended
1716
-	 * to every two hax figures.
1717
-	 *
1718
-	 * If an invalid string is passed, it will be returned without change.
1719
-	 *
1720
-	 * @param string $guid
1721
-	 * @return string
1722
-	 */
1723
-	public function formatGuid2ForFilterUser($guid) {
1724
-		if(!is_string($guid)) {
1725
-			throw new \InvalidArgumentException('String expected');
1726
-		}
1727
-		$blocks = explode('-', $guid);
1728
-		if(count($blocks) !== 5) {
1729
-			/*
1232
+        $findings = [];
1233
+        $savedoffset = $offset;
1234
+        do {
1235
+            $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1236
+            if($search === false) {
1237
+                return [];
1238
+            }
1239
+            list($sr, $pagedSearchOK) = $search;
1240
+            $cr = $this->connection->getConnectionResource();
1241
+
1242
+            if($skipHandling) {
1243
+                //i.e. result do not need to be fetched, we just need the cookie
1244
+                //thus pass 1 or any other value as $iFoundItems because it is not
1245
+                //used
1246
+                $this->processPagedSearchStatus($sr, $filter, $base, 1, $limitPerPage,
1247
+                                $offset, $pagedSearchOK,
1248
+                                $skipHandling);
1249
+                return array();
1250
+            }
1251
+
1252
+            $iFoundItems = 0;
1253
+            foreach($sr as $res) {
1254
+                $findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $res));
1255
+                $iFoundItems = max($iFoundItems, $findings['count']);
1256
+                unset($findings['count']);
1257
+            }
1258
+
1259
+            $continue = $this->processPagedSearchStatus($sr, $filter, $base, $iFoundItems,
1260
+                $limitPerPage, $offset, $pagedSearchOK,
1261
+                                        $skipHandling);
1262
+            $offset += $limitPerPage;
1263
+        } while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit));
1264
+        // reseting offset
1265
+        $offset = $savedoffset;
1266
+
1267
+        // if we're here, probably no connection resource is returned.
1268
+        // to make Nextcloud behave nicely, we simply give back an empty array.
1269
+        if(is_null($findings)) {
1270
+            return array();
1271
+        }
1272
+
1273
+        if(!is_null($attr)) {
1274
+            $selection = [];
1275
+            $i = 0;
1276
+            foreach($findings as $item) {
1277
+                if(!is_array($item)) {
1278
+                    continue;
1279
+                }
1280
+                $item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1281
+                foreach($attr as $key) {
1282
+                    if(isset($item[$key])) {
1283
+                        if(is_array($item[$key]) && isset($item[$key]['count'])) {
1284
+                            unset($item[$key]['count']);
1285
+                        }
1286
+                        if($key !== 'dn') {
1287
+                            if($this->resemblesDN($key)) {
1288
+                                $selection[$i][$key] = $this->helper->sanitizeDN($item[$key]);
1289
+                            } else if($key === 'objectguid' || $key === 'guid') {
1290
+                                $selection[$i][$key] = [$this->convertObjectGUID2Str($item[$key][0])];
1291
+                            } else {
1292
+                                $selection[$i][$key] = $item[$key];
1293
+                            }
1294
+                        } else {
1295
+                            $selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
1296
+                        }
1297
+                    }
1298
+
1299
+                }
1300
+                $i++;
1301
+            }
1302
+            $findings = $selection;
1303
+        }
1304
+        //we slice the findings, when
1305
+        //a) paged search unsuccessful, though attempted
1306
+        //b) no paged search, but limit set
1307
+        if((!$this->getPagedSearchResultState()
1308
+            && $pagedSearchOK)
1309
+            || (
1310
+                !$pagedSearchOK
1311
+                && !is_null($limit)
1312
+            )
1313
+        ) {
1314
+            $findings = array_slice($findings, (int)$offset, $limit);
1315
+        }
1316
+        return $findings;
1317
+    }
1318
+
1319
+    /**
1320
+     * @param string $name
1321
+     * @return string
1322
+     * @throws \InvalidArgumentException
1323
+     */
1324
+    public function sanitizeUsername($name) {
1325
+        $name = trim($name);
1326
+
1327
+        if($this->connection->ldapIgnoreNamingRules) {
1328
+            return $name;
1329
+        }
1330
+
1331
+        // Transliteration to ASCII
1332
+        $transliterated = @iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1333
+        if($transliterated !== false) {
1334
+            // depending on system config iconv can work or not
1335
+            $name = $transliterated;
1336
+        }
1337
+
1338
+        // Replacements
1339
+        $name = str_replace(' ', '_', $name);
1340
+
1341
+        // Every remaining disallowed characters will be removed
1342
+        $name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1343
+
1344
+        if($name === '') {
1345
+            throw new \InvalidArgumentException('provided name template for username does not contain any allowed characters');
1346
+        }
1347
+
1348
+        return $name;
1349
+    }
1350
+
1351
+    /**
1352
+     * escapes (user provided) parts for LDAP filter
1353
+     * @param string $input, the provided value
1354
+     * @param bool $allowAsterisk whether in * at the beginning should be preserved
1355
+     * @return string the escaped string
1356
+     */
1357
+    public function escapeFilterPart($input, $allowAsterisk = false) {
1358
+        $asterisk = '';
1359
+        if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1360
+            $asterisk = '*';
1361
+            $input = mb_substr($input, 1, null, 'UTF-8');
1362
+        }
1363
+        $search  = array('*', '\\', '(', ')');
1364
+        $replace = array('\\*', '\\\\', '\\(', '\\)');
1365
+        return $asterisk . str_replace($search, $replace, $input);
1366
+    }
1367
+
1368
+    /**
1369
+     * combines the input filters with AND
1370
+     * @param string[] $filters the filters to connect
1371
+     * @return string the combined filter
1372
+     */
1373
+    public function combineFilterWithAnd($filters) {
1374
+        return $this->combineFilter($filters, '&');
1375
+    }
1376
+
1377
+    /**
1378
+     * combines the input filters with OR
1379
+     * @param string[] $filters the filters to connect
1380
+     * @return string the combined filter
1381
+     * Combines Filter arguments with OR
1382
+     */
1383
+    public function combineFilterWithOr($filters) {
1384
+        return $this->combineFilter($filters, '|');
1385
+    }
1386
+
1387
+    /**
1388
+     * combines the input filters with given operator
1389
+     * @param string[] $filters the filters to connect
1390
+     * @param string $operator either & or |
1391
+     * @return string the combined filter
1392
+     */
1393
+    private function combineFilter($filters, $operator) {
1394
+        $combinedFilter = '('.$operator;
1395
+        foreach($filters as $filter) {
1396
+            if ($filter !== '' && $filter[0] !== '(') {
1397
+                $filter = '('.$filter.')';
1398
+            }
1399
+            $combinedFilter.=$filter;
1400
+        }
1401
+        $combinedFilter.=')';
1402
+        return $combinedFilter;
1403
+    }
1404
+
1405
+    /**
1406
+     * creates a filter part for to perform search for users
1407
+     * @param string $search the search term
1408
+     * @return string the final filter part to use in LDAP searches
1409
+     */
1410
+    public function getFilterPartForUserSearch($search) {
1411
+        return $this->getFilterPartForSearch($search,
1412
+            $this->connection->ldapAttributesForUserSearch,
1413
+            $this->connection->ldapUserDisplayName);
1414
+    }
1415
+
1416
+    /**
1417
+     * creates a filter part for to perform search for groups
1418
+     * @param string $search the search term
1419
+     * @return string the final filter part to use in LDAP searches
1420
+     */
1421
+    public function getFilterPartForGroupSearch($search) {
1422
+        return $this->getFilterPartForSearch($search,
1423
+            $this->connection->ldapAttributesForGroupSearch,
1424
+            $this->connection->ldapGroupDisplayName);
1425
+    }
1426
+
1427
+    /**
1428
+     * creates a filter part for searches by splitting up the given search
1429
+     * string into single words
1430
+     * @param string $search the search term
1431
+     * @param string[] $searchAttributes needs to have at least two attributes,
1432
+     * otherwise it does not make sense :)
1433
+     * @return string the final filter part to use in LDAP searches
1434
+     * @throws \Exception
1435
+     */
1436
+    private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1437
+        if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
1438
+            throw new \Exception('searchAttributes must be an array with at least two string');
1439
+        }
1440
+        $searchWords = explode(' ', trim($search));
1441
+        $wordFilters = array();
1442
+        foreach($searchWords as $word) {
1443
+            $word = $this->prepareSearchTerm($word);
1444
+            //every word needs to appear at least once
1445
+            $wordMatchOneAttrFilters = array();
1446
+            foreach($searchAttributes as $attr) {
1447
+                $wordMatchOneAttrFilters[] = $attr . '=' . $word;
1448
+            }
1449
+            $wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1450
+        }
1451
+        return $this->combineFilterWithAnd($wordFilters);
1452
+    }
1453
+
1454
+    /**
1455
+     * creates a filter part for searches
1456
+     * @param string $search the search term
1457
+     * @param string[]|null $searchAttributes
1458
+     * @param string $fallbackAttribute a fallback attribute in case the user
1459
+     * did not define search attributes. Typically the display name attribute.
1460
+     * @return string the final filter part to use in LDAP searches
1461
+     */
1462
+    private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1463
+        $filter = array();
1464
+        $haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1465
+        if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1466
+            try {
1467
+                return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1468
+            } catch(\Exception $e) {
1469
+                \OCP\Util::writeLog(
1470
+                    'user_ldap',
1471
+                    'Creating advanced filter for search failed, falling back to simple method.',
1472
+                    ILogger::INFO
1473
+                );
1474
+            }
1475
+        }
1476
+
1477
+        $search = $this->prepareSearchTerm($search);
1478
+        if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
1479
+            if ($fallbackAttribute === '') {
1480
+                return '';
1481
+            }
1482
+            $filter[] = $fallbackAttribute . '=' . $search;
1483
+        } else {
1484
+            foreach($searchAttributes as $attribute) {
1485
+                $filter[] = $attribute . '=' . $search;
1486
+            }
1487
+        }
1488
+        if(count($filter) === 1) {
1489
+            return '('.$filter[0].')';
1490
+        }
1491
+        return $this->combineFilterWithOr($filter);
1492
+    }
1493
+
1494
+    /**
1495
+     * returns the search term depending on whether we are allowed
1496
+     * list users found by ldap with the current input appended by
1497
+     * a *
1498
+     * @return string
1499
+     */
1500
+    private function prepareSearchTerm($term) {
1501
+        $config = \OC::$server->getConfig();
1502
+
1503
+        $allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
1504
+
1505
+        $result = $term;
1506
+        if ($term === '') {
1507
+            $result = '*';
1508
+        } else if ($allowEnum !== 'no') {
1509
+            $result = $term . '*';
1510
+        }
1511
+        return $result;
1512
+    }
1513
+
1514
+    /**
1515
+     * returns the filter used for counting users
1516
+     * @return string
1517
+     */
1518
+    public function getFilterForUserCount() {
1519
+        $filter = $this->combineFilterWithAnd(array(
1520
+            $this->connection->ldapUserFilter,
1521
+            $this->connection->ldapUserDisplayName . '=*'
1522
+        ));
1523
+
1524
+        return $filter;
1525
+    }
1526
+
1527
+    /**
1528
+     * @param string $name
1529
+     * @param string $password
1530
+     * @return bool
1531
+     */
1532
+    public function areCredentialsValid($name, $password) {
1533
+        $name = $this->helper->DNasBaseParameter($name);
1534
+        $testConnection = clone $this->connection;
1535
+        $credentials = array(
1536
+            'ldapAgentName' => $name,
1537
+            'ldapAgentPassword' => $password
1538
+        );
1539
+        if(!$testConnection->setConfiguration($credentials)) {
1540
+            return false;
1541
+        }
1542
+        return $testConnection->bind();
1543
+    }
1544
+
1545
+    /**
1546
+     * reverse lookup of a DN given a known UUID
1547
+     *
1548
+     * @param string $uuid
1549
+     * @return string
1550
+     * @throws \Exception
1551
+     */
1552
+    public function getUserDnByUuid($uuid) {
1553
+        $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1554
+        $filter       = $this->connection->ldapUserFilter;
1555
+        $base         = $this->connection->ldapBaseUsers;
1556
+
1557
+        if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
1558
+            // Sacrebleu! The UUID attribute is unknown :( We need first an
1559
+            // existing DN to be able to reliably detect it.
1560
+            $result = $this->search($filter, $base, ['dn'], 1);
1561
+            if(!isset($result[0]) || !isset($result[0]['dn'])) {
1562
+                throw new \Exception('Cannot determine UUID attribute');
1563
+            }
1564
+            $dn = $result[0]['dn'][0];
1565
+            if(!$this->detectUuidAttribute($dn, true)) {
1566
+                throw new \Exception('Cannot determine UUID attribute');
1567
+            }
1568
+        } else {
1569
+            // The UUID attribute is either known or an override is given.
1570
+            // By calling this method we ensure that $this->connection->$uuidAttr
1571
+            // is definitely set
1572
+            if(!$this->detectUuidAttribute('', true)) {
1573
+                throw new \Exception('Cannot determine UUID attribute');
1574
+            }
1575
+        }
1576
+
1577
+        $uuidAttr = $this->connection->ldapUuidUserAttribute;
1578
+        if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1579
+            $uuid = $this->formatGuid2ForFilterUser($uuid);
1580
+        }
1581
+
1582
+        $filter = $uuidAttr . '=' . $uuid;
1583
+        $result = $this->searchUsers($filter, ['dn'], 2);
1584
+        if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1585
+            // we put the count into account to make sure that this is
1586
+            // really unique
1587
+            return $result[0]['dn'][0];
1588
+        }
1589
+
1590
+        throw new \Exception('Cannot determine UUID attribute');
1591
+    }
1592
+
1593
+    /**
1594
+     * auto-detects the directory's UUID attribute
1595
+     *
1596
+     * @param string $dn a known DN used to check against
1597
+     * @param bool $isUser
1598
+     * @param bool $force the detection should be run, even if it is not set to auto
1599
+     * @param array|null $ldapRecord
1600
+     * @return bool true on success, false otherwise
1601
+     */
1602
+    private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) {
1603
+        if($isUser) {
1604
+            $uuidAttr     = 'ldapUuidUserAttribute';
1605
+            $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1606
+        } else {
1607
+            $uuidAttr     = 'ldapUuidGroupAttribute';
1608
+            $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1609
+        }
1610
+
1611
+        if(($this->connection->$uuidAttr !== 'auto') && !$force) {
1612
+            return true;
1613
+        }
1614
+
1615
+        if (is_string($uuidOverride) && trim($uuidOverride) !== '' && !$force) {
1616
+            $this->connection->$uuidAttr = $uuidOverride;
1617
+            return true;
1618
+        }
1619
+
1620
+        foreach(self::UUID_ATTRIBUTES as $attribute) {
1621
+            if($ldapRecord !== null) {
1622
+                // we have the info from LDAP already, we don't need to talk to the server again
1623
+                if(isset($ldapRecord[$attribute])) {
1624
+                    $this->connection->$uuidAttr = $attribute;
1625
+                    return true;
1626
+                } else {
1627
+                    continue;
1628
+                }
1629
+            }
1630
+
1631
+            $value = $this->readAttribute($dn, $attribute);
1632
+            if(is_array($value) && isset($value[0]) && !empty($value[0])) {
1633
+                \OCP\Util::writeLog(
1634
+                    'user_ldap',
1635
+                    'Setting '.$attribute.' as '.$uuidAttr,
1636
+                    ILogger::DEBUG
1637
+                );
1638
+                $this->connection->$uuidAttr = $attribute;
1639
+                return true;
1640
+            }
1641
+        }
1642
+        \OCP\Util::writeLog(
1643
+            'user_ldap',
1644
+            'Could not autodetect the UUID attribute',
1645
+            ILogger::ERROR
1646
+        );
1647
+
1648
+        return false;
1649
+    }
1650
+
1651
+    /**
1652
+     * @param string $dn
1653
+     * @param bool $isUser
1654
+     * @param null $ldapRecord
1655
+     * @return bool|string
1656
+     */
1657
+    public function getUUID($dn, $isUser = true, $ldapRecord = null) {
1658
+        if($isUser) {
1659
+            $uuidAttr     = 'ldapUuidUserAttribute';
1660
+            $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1661
+        } else {
1662
+            $uuidAttr     = 'ldapUuidGroupAttribute';
1663
+            $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1664
+        }
1665
+
1666
+        $uuid = false;
1667
+        if($this->detectUuidAttribute($dn, $isUser, false, $ldapRecord)) {
1668
+            $attr = $this->connection->$uuidAttr;
1669
+            $uuid = isset($ldapRecord[$attr]) ? $ldapRecord[$attr] : $this->readAttribute($dn, $attr);
1670
+            if( !is_array($uuid)
1671
+                && $uuidOverride !== ''
1672
+                && $this->detectUuidAttribute($dn, $isUser, true, $ldapRecord))
1673
+            {
1674
+                $uuid = isset($ldapRecord[$this->connection->$uuidAttr])
1675
+                    ? $ldapRecord[$this->connection->$uuidAttr]
1676
+                    : $this->readAttribute($dn, $this->connection->$uuidAttr);
1677
+            }
1678
+            if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
1679
+                $uuid = $uuid[0];
1680
+            }
1681
+        }
1682
+
1683
+        return $uuid;
1684
+    }
1685
+
1686
+    /**
1687
+     * converts a binary ObjectGUID into a string representation
1688
+     * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
1689
+     * @return string
1690
+     * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
1691
+     */
1692
+    private function convertObjectGUID2Str($oguid) {
1693
+        $hex_guid = bin2hex($oguid);
1694
+        $hex_guid_to_guid_str = '';
1695
+        for($k = 1; $k <= 4; ++$k) {
1696
+            $hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
1697
+        }
1698
+        $hex_guid_to_guid_str .= '-';
1699
+        for($k = 1; $k <= 2; ++$k) {
1700
+            $hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
1701
+        }
1702
+        $hex_guid_to_guid_str .= '-';
1703
+        for($k = 1; $k <= 2; ++$k) {
1704
+            $hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
1705
+        }
1706
+        $hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
1707
+        $hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
1708
+
1709
+        return strtoupper($hex_guid_to_guid_str);
1710
+    }
1711
+
1712
+    /**
1713
+     * the first three blocks of the string-converted GUID happen to be in
1714
+     * reverse order. In order to use it in a filter, this needs to be
1715
+     * corrected. Furthermore the dashes need to be replaced and \\ preprended
1716
+     * to every two hax figures.
1717
+     *
1718
+     * If an invalid string is passed, it will be returned without change.
1719
+     *
1720
+     * @param string $guid
1721
+     * @return string
1722
+     */
1723
+    public function formatGuid2ForFilterUser($guid) {
1724
+        if(!is_string($guid)) {
1725
+            throw new \InvalidArgumentException('String expected');
1726
+        }
1727
+        $blocks = explode('-', $guid);
1728
+        if(count($blocks) !== 5) {
1729
+            /*
1730 1730
 			 * Why not throw an Exception instead? This method is a utility
1731 1731
 			 * called only when trying to figure out whether a "missing" known
1732 1732
 			 * LDAP user was or was not renamed on the LDAP server. And this
@@ -1737,270 +1737,270 @@  discard block
 block discarded – undo
1737 1737
 			 * an exception here would kill the experience for a valid, acting
1738 1738
 			 * user. Instead we write a log message.
1739 1739
 			 */
1740
-			\OC::$server->getLogger()->info(
1741
-				'Passed string does not resemble a valid GUID. Known UUID ' .
1742
-				'({uuid}) probably does not match UUID configuration.',
1743
-				[ 'app' => 'user_ldap', 'uuid' => $guid ]
1744
-			);
1745
-			return $guid;
1746
-		}
1747
-		for($i=0; $i < 3; $i++) {
1748
-			$pairs = str_split($blocks[$i], 2);
1749
-			$pairs = array_reverse($pairs);
1750
-			$blocks[$i] = implode('', $pairs);
1751
-		}
1752
-		for($i=0; $i < 5; $i++) {
1753
-			$pairs = str_split($blocks[$i], 2);
1754
-			$blocks[$i] = '\\' . implode('\\', $pairs);
1755
-		}
1756
-		return implode('', $blocks);
1757
-	}
1758
-
1759
-	/**
1760
-	 * gets a SID of the domain of the given dn
1761
-	 * @param string $dn
1762
-	 * @return string|bool
1763
-	 */
1764
-	public function getSID($dn) {
1765
-		$domainDN = $this->getDomainDNFromDN($dn);
1766
-		$cacheKey = 'getSID-'.$domainDN;
1767
-		$sid = $this->connection->getFromCache($cacheKey);
1768
-		if(!is_null($sid)) {
1769
-			return $sid;
1770
-		}
1771
-
1772
-		$objectSid = $this->readAttribute($domainDN, 'objectsid');
1773
-		if(!is_array($objectSid) || empty($objectSid)) {
1774
-			$this->connection->writeToCache($cacheKey, false);
1775
-			return false;
1776
-		}
1777
-		$domainObjectSid = $this->convertSID2Str($objectSid[0]);
1778
-		$this->connection->writeToCache($cacheKey, $domainObjectSid);
1779
-
1780
-		return $domainObjectSid;
1781
-	}
1782
-
1783
-	/**
1784
-	 * converts a binary SID into a string representation
1785
-	 * @param string $sid
1786
-	 * @return string
1787
-	 */
1788
-	public function convertSID2Str($sid) {
1789
-		// The format of a SID binary string is as follows:
1790
-		// 1 byte for the revision level
1791
-		// 1 byte for the number n of variable sub-ids
1792
-		// 6 bytes for identifier authority value
1793
-		// n*4 bytes for n sub-ids
1794
-		//
1795
-		// Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
1796
-		//  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
1797
-		$revision = ord($sid[0]);
1798
-		$numberSubID = ord($sid[1]);
1799
-
1800
-		$subIdStart = 8; // 1 + 1 + 6
1801
-		$subIdLength = 4;
1802
-		if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
1803
-			// Incorrect number of bytes present.
1804
-			return '';
1805
-		}
1806
-
1807
-		// 6 bytes = 48 bits can be represented using floats without loss of
1808
-		// precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
1809
-		$iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
1810
-
1811
-		$subIDs = array();
1812
-		for ($i = 0; $i < $numberSubID; $i++) {
1813
-			$subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
1814
-			$subIDs[] = sprintf('%u', $subID[1]);
1815
-		}
1816
-
1817
-		// Result for example above: S-1-5-21-249921958-728525901-1594176202
1818
-		return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
1819
-	}
1820
-
1821
-	/**
1822
-	 * checks if the given DN is part of the given base DN(s)
1823
-	 * @param string $dn the DN
1824
-	 * @param string[] $bases array containing the allowed base DN or DNs
1825
-	 * @return bool
1826
-	 */
1827
-	public function isDNPartOfBase($dn, $bases) {
1828
-		$belongsToBase = false;
1829
-		$bases = $this->helper->sanitizeDN($bases);
1830
-
1831
-		foreach($bases as $base) {
1832
-			$belongsToBase = true;
1833
-			if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) {
1834
-				$belongsToBase = false;
1835
-			}
1836
-			if($belongsToBase) {
1837
-				break;
1838
-			}
1839
-		}
1840
-		return $belongsToBase;
1841
-	}
1842
-
1843
-	/**
1844
-	 * resets a running Paged Search operation
1845
-	 */
1846
-	private function abandonPagedSearch() {
1847
-		if($this->connection->hasPagedResultSupport) {
1848
-			$cr = $this->connection->getConnectionResource();
1849
-			$this->invokeLDAPMethod('controlPagedResult', $cr, 0, false, $this->lastCookie);
1850
-			$this->getPagedSearchResultState();
1851
-			$this->lastCookie = '';
1852
-			$this->cookies = array();
1853
-		}
1854
-	}
1855
-
1856
-	/**
1857
-	 * get a cookie for the next LDAP paged search
1858
-	 * @param string $base a string with the base DN for the search
1859
-	 * @param string $filter the search filter to identify the correct search
1860
-	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1861
-	 * @param int $offset the offset for the new search to identify the correct search really good
1862
-	 * @return string containing the key or empty if none is cached
1863
-	 */
1864
-	private function getPagedResultCookie($base, $filter, $limit, $offset) {
1865
-		if($offset === 0) {
1866
-			return '';
1867
-		}
1868
-		$offset -= $limit;
1869
-		//we work with cache here
1870
-		$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset;
1871
-		$cookie = '';
1872
-		if(isset($this->cookies[$cacheKey])) {
1873
-			$cookie = $this->cookies[$cacheKey];
1874
-			if(is_null($cookie)) {
1875
-				$cookie = '';
1876
-			}
1877
-		}
1878
-		return $cookie;
1879
-	}
1880
-
1881
-	/**
1882
-	 * checks whether an LDAP paged search operation has more pages that can be
1883
-	 * retrieved, typically when offset and limit are provided.
1884
-	 *
1885
-	 * Be very careful to use it: the last cookie value, which is inspected, can
1886
-	 * be reset by other operations. Best, call it immediately after a search(),
1887
-	 * searchUsers() or searchGroups() call. count-methods are probably safe as
1888
-	 * well. Don't rely on it with any fetchList-method.
1889
-	 * @return bool
1890
-	 */
1891
-	public function hasMoreResults() {
1892
-		if(!$this->connection->hasPagedResultSupport) {
1893
-			return false;
1894
-		}
1895
-
1896
-		if(empty($this->lastCookie) && $this->lastCookie !== '0') {
1897
-			// as in RFC 2696, when all results are returned, the cookie will
1898
-			// be empty.
1899
-			return false;
1900
-		}
1901
-
1902
-		return true;
1903
-	}
1904
-
1905
-	/**
1906
-	 * set a cookie for LDAP paged search run
1907
-	 * @param string $base a string with the base DN for the search
1908
-	 * @param string $filter the search filter to identify the correct search
1909
-	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1910
-	 * @param int $offset the offset for the run search to identify the correct search really good
1911
-	 * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response
1912
-	 * @return void
1913
-	 */
1914
-	private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) {
1915
-		// allow '0' for 389ds
1916
-		if(!empty($cookie) || $cookie === '0') {
1917
-			$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset;
1918
-			$this->cookies[$cacheKey] = $cookie;
1919
-			$this->lastCookie = $cookie;
1920
-		}
1921
-	}
1922
-
1923
-	/**
1924
-	 * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
1925
-	 * @return boolean|null true on success, null or false otherwise
1926
-	 */
1927
-	public function getPagedSearchResultState() {
1928
-		$result = $this->pagedSearchedSuccessful;
1929
-		$this->pagedSearchedSuccessful = null;
1930
-		return $result;
1931
-	}
1932
-
1933
-	/**
1934
-	 * Prepares a paged search, if possible
1935
-	 * @param string $filter the LDAP filter for the search
1936
-	 * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
1937
-	 * @param string[] $attr optional, when a certain attribute shall be filtered outside
1938
-	 * @param int $limit
1939
-	 * @param int $offset
1940
-	 * @return bool|true
1941
-	 */
1942
-	private function initPagedSearch($filter, $bases, $attr, $limit, $offset) {
1943
-		$pagedSearchOK = false;
1944
-		if($this->connection->hasPagedResultSupport && ($limit !== 0)) {
1945
-			$offset = (int)$offset; //can be null
1946
-			\OCP\Util::writeLog('user_ldap',
1947
-				'initializing paged search for  Filter '.$filter.' base '.print_r($bases, true)
1948
-				.' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
1949
-				ILogger::DEBUG);
1950
-			//get the cookie from the search for the previous search, required by LDAP
1951
-			foreach($bases as $base) {
1952
-
1953
-				$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1954
-				if(empty($cookie) && $cookie !== "0" && ($offset > 0)) {
1955
-					// no cookie known from a potential previous search. We need
1956
-					// to start from 0 to come to the desired page. cookie value
1957
-					// of '0' is valid, because 389ds
1958
-					$reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
1959
-					$this->search($filter, array($base), $attr, $limit, $reOffset, true);
1960
-					$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1961
-					//still no cookie? obviously, the server does not like us. Let's skip paging efforts.
1962
-					// '0' is valid, because 389ds
1963
-					//TODO: remember this, probably does not change in the next request...
1964
-					if(empty($cookie) && $cookie !== '0') {
1965
-						$cookie = null;
1966
-					}
1967
-				}
1968
-				if(!is_null($cookie)) {
1969
-					//since offset = 0, this is a new search. We abandon other searches that might be ongoing.
1970
-					$this->abandonPagedSearch();
1971
-					$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
1972
-						$this->connection->getConnectionResource(), $limit,
1973
-						false, $cookie);
1974
-					if(!$pagedSearchOK) {
1975
-						return false;
1976
-					}
1977
-					\OCP\Util::writeLog('user_ldap', 'Ready for a paged search', ILogger::DEBUG);
1978
-				} else {
1979
-					$e = new \Exception('No paged search possible, Limit '.$limit.' Offset '.$offset);
1980
-					\OC::$server->getLogger()->logException($e, ['level' => ILogger::DEBUG]);
1981
-				}
1982
-
1983
-			}
1984
-		/* ++ Fixing RHDS searches with pages with zero results ++
1740
+            \OC::$server->getLogger()->info(
1741
+                'Passed string does not resemble a valid GUID. Known UUID ' .
1742
+                '({uuid}) probably does not match UUID configuration.',
1743
+                [ 'app' => 'user_ldap', 'uuid' => $guid ]
1744
+            );
1745
+            return $guid;
1746
+        }
1747
+        for($i=0; $i < 3; $i++) {
1748
+            $pairs = str_split($blocks[$i], 2);
1749
+            $pairs = array_reverse($pairs);
1750
+            $blocks[$i] = implode('', $pairs);
1751
+        }
1752
+        for($i=0; $i < 5; $i++) {
1753
+            $pairs = str_split($blocks[$i], 2);
1754
+            $blocks[$i] = '\\' . implode('\\', $pairs);
1755
+        }
1756
+        return implode('', $blocks);
1757
+    }
1758
+
1759
+    /**
1760
+     * gets a SID of the domain of the given dn
1761
+     * @param string $dn
1762
+     * @return string|bool
1763
+     */
1764
+    public function getSID($dn) {
1765
+        $domainDN = $this->getDomainDNFromDN($dn);
1766
+        $cacheKey = 'getSID-'.$domainDN;
1767
+        $sid = $this->connection->getFromCache($cacheKey);
1768
+        if(!is_null($sid)) {
1769
+            return $sid;
1770
+        }
1771
+
1772
+        $objectSid = $this->readAttribute($domainDN, 'objectsid');
1773
+        if(!is_array($objectSid) || empty($objectSid)) {
1774
+            $this->connection->writeToCache($cacheKey, false);
1775
+            return false;
1776
+        }
1777
+        $domainObjectSid = $this->convertSID2Str($objectSid[0]);
1778
+        $this->connection->writeToCache($cacheKey, $domainObjectSid);
1779
+
1780
+        return $domainObjectSid;
1781
+    }
1782
+
1783
+    /**
1784
+     * converts a binary SID into a string representation
1785
+     * @param string $sid
1786
+     * @return string
1787
+     */
1788
+    public function convertSID2Str($sid) {
1789
+        // The format of a SID binary string is as follows:
1790
+        // 1 byte for the revision level
1791
+        // 1 byte for the number n of variable sub-ids
1792
+        // 6 bytes for identifier authority value
1793
+        // n*4 bytes for n sub-ids
1794
+        //
1795
+        // Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
1796
+        //  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
1797
+        $revision = ord($sid[0]);
1798
+        $numberSubID = ord($sid[1]);
1799
+
1800
+        $subIdStart = 8; // 1 + 1 + 6
1801
+        $subIdLength = 4;
1802
+        if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
1803
+            // Incorrect number of bytes present.
1804
+            return '';
1805
+        }
1806
+
1807
+        // 6 bytes = 48 bits can be represented using floats without loss of
1808
+        // precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
1809
+        $iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
1810
+
1811
+        $subIDs = array();
1812
+        for ($i = 0; $i < $numberSubID; $i++) {
1813
+            $subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
1814
+            $subIDs[] = sprintf('%u', $subID[1]);
1815
+        }
1816
+
1817
+        // Result for example above: S-1-5-21-249921958-728525901-1594176202
1818
+        return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
1819
+    }
1820
+
1821
+    /**
1822
+     * checks if the given DN is part of the given base DN(s)
1823
+     * @param string $dn the DN
1824
+     * @param string[] $bases array containing the allowed base DN or DNs
1825
+     * @return bool
1826
+     */
1827
+    public function isDNPartOfBase($dn, $bases) {
1828
+        $belongsToBase = false;
1829
+        $bases = $this->helper->sanitizeDN($bases);
1830
+
1831
+        foreach($bases as $base) {
1832
+            $belongsToBase = true;
1833
+            if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) {
1834
+                $belongsToBase = false;
1835
+            }
1836
+            if($belongsToBase) {
1837
+                break;
1838
+            }
1839
+        }
1840
+        return $belongsToBase;
1841
+    }
1842
+
1843
+    /**
1844
+     * resets a running Paged Search operation
1845
+     */
1846
+    private function abandonPagedSearch() {
1847
+        if($this->connection->hasPagedResultSupport) {
1848
+            $cr = $this->connection->getConnectionResource();
1849
+            $this->invokeLDAPMethod('controlPagedResult', $cr, 0, false, $this->lastCookie);
1850
+            $this->getPagedSearchResultState();
1851
+            $this->lastCookie = '';
1852
+            $this->cookies = array();
1853
+        }
1854
+    }
1855
+
1856
+    /**
1857
+     * get a cookie for the next LDAP paged search
1858
+     * @param string $base a string with the base DN for the search
1859
+     * @param string $filter the search filter to identify the correct search
1860
+     * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1861
+     * @param int $offset the offset for the new search to identify the correct search really good
1862
+     * @return string containing the key or empty if none is cached
1863
+     */
1864
+    private function getPagedResultCookie($base, $filter, $limit, $offset) {
1865
+        if($offset === 0) {
1866
+            return '';
1867
+        }
1868
+        $offset -= $limit;
1869
+        //we work with cache here
1870
+        $cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset;
1871
+        $cookie = '';
1872
+        if(isset($this->cookies[$cacheKey])) {
1873
+            $cookie = $this->cookies[$cacheKey];
1874
+            if(is_null($cookie)) {
1875
+                $cookie = '';
1876
+            }
1877
+        }
1878
+        return $cookie;
1879
+    }
1880
+
1881
+    /**
1882
+     * checks whether an LDAP paged search operation has more pages that can be
1883
+     * retrieved, typically when offset and limit are provided.
1884
+     *
1885
+     * Be very careful to use it: the last cookie value, which is inspected, can
1886
+     * be reset by other operations. Best, call it immediately after a search(),
1887
+     * searchUsers() or searchGroups() call. count-methods are probably safe as
1888
+     * well. Don't rely on it with any fetchList-method.
1889
+     * @return bool
1890
+     */
1891
+    public function hasMoreResults() {
1892
+        if(!$this->connection->hasPagedResultSupport) {
1893
+            return false;
1894
+        }
1895
+
1896
+        if(empty($this->lastCookie) && $this->lastCookie !== '0') {
1897
+            // as in RFC 2696, when all results are returned, the cookie will
1898
+            // be empty.
1899
+            return false;
1900
+        }
1901
+
1902
+        return true;
1903
+    }
1904
+
1905
+    /**
1906
+     * set a cookie for LDAP paged search run
1907
+     * @param string $base a string with the base DN for the search
1908
+     * @param string $filter the search filter to identify the correct search
1909
+     * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1910
+     * @param int $offset the offset for the run search to identify the correct search really good
1911
+     * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response
1912
+     * @return void
1913
+     */
1914
+    private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) {
1915
+        // allow '0' for 389ds
1916
+        if(!empty($cookie) || $cookie === '0') {
1917
+            $cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset;
1918
+            $this->cookies[$cacheKey] = $cookie;
1919
+            $this->lastCookie = $cookie;
1920
+        }
1921
+    }
1922
+
1923
+    /**
1924
+     * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
1925
+     * @return boolean|null true on success, null or false otherwise
1926
+     */
1927
+    public function getPagedSearchResultState() {
1928
+        $result = $this->pagedSearchedSuccessful;
1929
+        $this->pagedSearchedSuccessful = null;
1930
+        return $result;
1931
+    }
1932
+
1933
+    /**
1934
+     * Prepares a paged search, if possible
1935
+     * @param string $filter the LDAP filter for the search
1936
+     * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
1937
+     * @param string[] $attr optional, when a certain attribute shall be filtered outside
1938
+     * @param int $limit
1939
+     * @param int $offset
1940
+     * @return bool|true
1941
+     */
1942
+    private function initPagedSearch($filter, $bases, $attr, $limit, $offset) {
1943
+        $pagedSearchOK = false;
1944
+        if($this->connection->hasPagedResultSupport && ($limit !== 0)) {
1945
+            $offset = (int)$offset; //can be null
1946
+            \OCP\Util::writeLog('user_ldap',
1947
+                'initializing paged search for  Filter '.$filter.' base '.print_r($bases, true)
1948
+                .' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
1949
+                ILogger::DEBUG);
1950
+            //get the cookie from the search for the previous search, required by LDAP
1951
+            foreach($bases as $base) {
1952
+
1953
+                $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1954
+                if(empty($cookie) && $cookie !== "0" && ($offset > 0)) {
1955
+                    // no cookie known from a potential previous search. We need
1956
+                    // to start from 0 to come to the desired page. cookie value
1957
+                    // of '0' is valid, because 389ds
1958
+                    $reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
1959
+                    $this->search($filter, array($base), $attr, $limit, $reOffset, true);
1960
+                    $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1961
+                    //still no cookie? obviously, the server does not like us. Let's skip paging efforts.
1962
+                    // '0' is valid, because 389ds
1963
+                    //TODO: remember this, probably does not change in the next request...
1964
+                    if(empty($cookie) && $cookie !== '0') {
1965
+                        $cookie = null;
1966
+                    }
1967
+                }
1968
+                if(!is_null($cookie)) {
1969
+                    //since offset = 0, this is a new search. We abandon other searches that might be ongoing.
1970
+                    $this->abandonPagedSearch();
1971
+                    $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
1972
+                        $this->connection->getConnectionResource(), $limit,
1973
+                        false, $cookie);
1974
+                    if(!$pagedSearchOK) {
1975
+                        return false;
1976
+                    }
1977
+                    \OCP\Util::writeLog('user_ldap', 'Ready for a paged search', ILogger::DEBUG);
1978
+                } else {
1979
+                    $e = new \Exception('No paged search possible, Limit '.$limit.' Offset '.$offset);
1980
+                    \OC::$server->getLogger()->logException($e, ['level' => ILogger::DEBUG]);
1981
+                }
1982
+
1983
+            }
1984
+        /* ++ Fixing RHDS searches with pages with zero results ++
1985 1985
 		 * We coudn't get paged searches working with our RHDS for login ($limit = 0),
1986 1986
 		 * due to pages with zero results.
1987 1987
 		 * So we added "&& !empty($this->lastCookie)" to this test to ignore pagination
1988 1988
 		 * if we don't have a previous paged search.
1989 1989
 		 */
1990
-		} else if($this->connection->hasPagedResultSupport && $limit === 0 && !empty($this->lastCookie)) {
1991
-			// a search without limit was requested. However, if we do use
1992
-			// Paged Search once, we always must do it. This requires us to
1993
-			// initialize it with the configured page size.
1994
-			$this->abandonPagedSearch();
1995
-			// in case someone set it to 0 … use 500, otherwise no results will
1996
-			// be returned.
1997
-			$pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500;
1998
-			$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
1999
-				$this->connection->getConnectionResource(),
2000
-				$pageSize, false, '');
2001
-		}
2002
-
2003
-		return $pagedSearchOK;
2004
-	}
1990
+        } else if($this->connection->hasPagedResultSupport && $limit === 0 && !empty($this->lastCookie)) {
1991
+            // a search without limit was requested. However, if we do use
1992
+            // Paged Search once, we always must do it. This requires us to
1993
+            // initialize it with the configured page size.
1994
+            $this->abandonPagedSearch();
1995
+            // in case someone set it to 0 … use 500, otherwise no results will
1996
+            // be returned.
1997
+            $pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500;
1998
+            $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
1999
+                $this->connection->getConnectionResource(),
2000
+                $pageSize, false, '');
2001
+        }
2002
+
2003
+        return $pagedSearchOK;
2004
+    }
2005 2005
 
2006 2006
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/User/User.php 1 patch
Indentation   +645 added lines, -645 removed lines patch added patch discarded remove patch
@@ -47,655 +47,655 @@
 block discarded – undo
47 47
  * represents an LDAP user, gets and holds user-specific information from LDAP
48 48
  */
49 49
 class User {
50
-	/**
51
-	 * @var IUserTools
52
-	 */
53
-	protected $access;
54
-	/**
55
-	 * @var Connection
56
-	 */
57
-	protected $connection;
58
-	/**
59
-	 * @var IConfig
60
-	 */
61
-	protected $config;
62
-	/**
63
-	 * @var FilesystemHelper
64
-	 */
65
-	protected $fs;
66
-	/**
67
-	 * @var Image
68
-	 */
69
-	protected $image;
70
-	/**
71
-	 * @var LogWrapper
72
-	 */
73
-	protected $log;
74
-	/**
75
-	 * @var IAvatarManager
76
-	 */
77
-	protected $avatarManager;
78
-	/**
79
-	 * @var IUserManager
80
-	 */
81
-	protected $userManager;
82
-	/**
83
-	 * @var INotificationManager
84
-	 */
85
-	protected $notificationManager;
86
-	/**
87
-	 * @var string
88
-	 */
89
-	protected $dn;
90
-	/**
91
-	 * @var string
92
-	 */
93
-	protected $uid;
94
-	/**
95
-	 * @var string[]
96
-	 */
97
-	protected $refreshedFeatures = array();
98
-	/**
99
-	 * @var string
100
-	 */
101
-	protected $avatarImage;
102
-
103
-	/**
104
-	 * DB config keys for user preferences
105
-	 */
106
-	const USER_PREFKEY_FIRSTLOGIN  = 'firstLoginAccomplished';
107
-	const USER_PREFKEY_LASTREFRESH = 'lastFeatureRefresh';
108
-
109
-	/**
110
-	 * @brief constructor, make sure the subclasses call this one!
111
-	 * @param string $username the internal username
112
-	 * @param string $dn the LDAP DN
113
-	 * @param IUserTools $access an instance that implements IUserTools for
114
-	 * LDAP interaction
115
-	 * @param IConfig $config
116
-	 * @param FilesystemHelper $fs
117
-	 * @param Image $image any empty instance
118
-	 * @param LogWrapper $log
119
-	 * @param IAvatarManager $avatarManager
120
-	 * @param IUserManager $userManager
121
-	 * @param INotificationManager $notificationManager
122
-	 */
123
-	public function __construct($username, $dn, IUserTools $access,
124
-		IConfig $config, FilesystemHelper $fs, Image $image,
125
-		LogWrapper $log, IAvatarManager $avatarManager, IUserManager $userManager,
126
-		INotificationManager $notificationManager) {
50
+    /**
51
+     * @var IUserTools
52
+     */
53
+    protected $access;
54
+    /**
55
+     * @var Connection
56
+     */
57
+    protected $connection;
58
+    /**
59
+     * @var IConfig
60
+     */
61
+    protected $config;
62
+    /**
63
+     * @var FilesystemHelper
64
+     */
65
+    protected $fs;
66
+    /**
67
+     * @var Image
68
+     */
69
+    protected $image;
70
+    /**
71
+     * @var LogWrapper
72
+     */
73
+    protected $log;
74
+    /**
75
+     * @var IAvatarManager
76
+     */
77
+    protected $avatarManager;
78
+    /**
79
+     * @var IUserManager
80
+     */
81
+    protected $userManager;
82
+    /**
83
+     * @var INotificationManager
84
+     */
85
+    protected $notificationManager;
86
+    /**
87
+     * @var string
88
+     */
89
+    protected $dn;
90
+    /**
91
+     * @var string
92
+     */
93
+    protected $uid;
94
+    /**
95
+     * @var string[]
96
+     */
97
+    protected $refreshedFeatures = array();
98
+    /**
99
+     * @var string
100
+     */
101
+    protected $avatarImage;
102
+
103
+    /**
104
+     * DB config keys for user preferences
105
+     */
106
+    const USER_PREFKEY_FIRSTLOGIN  = 'firstLoginAccomplished';
107
+    const USER_PREFKEY_LASTREFRESH = 'lastFeatureRefresh';
108
+
109
+    /**
110
+     * @brief constructor, make sure the subclasses call this one!
111
+     * @param string $username the internal username
112
+     * @param string $dn the LDAP DN
113
+     * @param IUserTools $access an instance that implements IUserTools for
114
+     * LDAP interaction
115
+     * @param IConfig $config
116
+     * @param FilesystemHelper $fs
117
+     * @param Image $image any empty instance
118
+     * @param LogWrapper $log
119
+     * @param IAvatarManager $avatarManager
120
+     * @param IUserManager $userManager
121
+     * @param INotificationManager $notificationManager
122
+     */
123
+    public function __construct($username, $dn, IUserTools $access,
124
+        IConfig $config, FilesystemHelper $fs, Image $image,
125
+        LogWrapper $log, IAvatarManager $avatarManager, IUserManager $userManager,
126
+        INotificationManager $notificationManager) {
127 127
 	
128
-		if ($username === null) {
129
-			$log->log("uid for '$dn' must not be null!", ILogger::ERROR);
130
-			throw new \InvalidArgumentException('uid must not be null!');
131
-		} else if ($username === '') {
132
-			$log->log("uid for '$dn' must not be an empty string", ILogger::ERROR);
133
-			throw new \InvalidArgumentException('uid must not be an empty string!');
134
-		}
135
-
136
-		$this->access              = $access;
137
-		$this->connection          = $access->getConnection();
138
-		$this->config              = $config;
139
-		$this->fs                  = $fs;
140
-		$this->dn                  = $dn;
141
-		$this->uid                 = $username;
142
-		$this->image               = $image;
143
-		$this->log                 = $log;
144
-		$this->avatarManager       = $avatarManager;
145
-		$this->userManager         = $userManager;
146
-		$this->notificationManager = $notificationManager;
147
-
148
-		\OCP\Util::connectHook('OC_User', 'post_login', $this, 'handlePasswordExpiry');
149
-	}
150
-
151
-	/**
152
-	 * @brief updates properties like email, quota or avatar provided by LDAP
153
-	 * @return null
154
-	 */
155
-	public function update() {
156
-		if(is_null($this->dn)) {
157
-			return null;
158
-		}
159
-
160
-		$hasLoggedIn = $this->config->getUserValue($this->uid, 'user_ldap',
161
-				self::USER_PREFKEY_FIRSTLOGIN, 0);
162
-
163
-		if($this->needsRefresh()) {
164
-			$this->updateEmail();
165
-			$this->updateQuota();
166
-			if($hasLoggedIn !== 0) {
167
-				//we do not need to try it, when the user has not been logged in
168
-				//before, because the file system will not be ready.
169
-				$this->updateAvatar();
170
-				//in order to get an avatar as soon as possible, mark the user
171
-				//as refreshed only when updating the avatar did happen
172
-				$this->markRefreshTime();
173
-			}
174
-		}
175
-	}
176
-
177
-	/**
178
-	 * processes results from LDAP for attributes as returned by getAttributesToRead()
179
-	 * @param array $ldapEntry the user entry as retrieved from LDAP
180
-	 */
181
-	public function processAttributes($ldapEntry) {
182
-		$this->markRefreshTime();
183
-		//Quota
184
-		$attr = strtolower($this->connection->ldapQuotaAttribute);
185
-		if(isset($ldapEntry[$attr])) {
186
-			$this->updateQuota($ldapEntry[$attr][0]);
187
-		} else {
188
-			if ($this->connection->ldapQuotaDefault !== '') {
189
-				$this->updateQuota();
190
-			}
191
-		}
192
-		unset($attr);
193
-
194
-		//displayName
195
-		$displayName = $displayName2 = '';
196
-		$attr = strtolower($this->connection->ldapUserDisplayName);
197
-		if(isset($ldapEntry[$attr])) {
198
-			$displayName = (string)$ldapEntry[$attr][0];
199
-		}
200
-		$attr = strtolower($this->connection->ldapUserDisplayName2);
201
-		if(isset($ldapEntry[$attr])) {
202
-			$displayName2 = (string)$ldapEntry[$attr][0];
203
-		}
204
-		if ($displayName !== '') {
205
-			$this->composeAndStoreDisplayName($displayName);
206
-			$this->access->cacheUserDisplayName(
207
-				$this->getUsername(),
208
-				$displayName,
209
-				$displayName2
210
-			);
211
-		}
212
-		unset($attr);
213
-
214
-		//Email
215
-		//email must be stored after displayname, because it would cause a user
216
-		//change event that will trigger fetching the display name again
217
-		$attr = strtolower($this->connection->ldapEmailAttribute);
218
-		if(isset($ldapEntry[$attr])) {
219
-			$this->updateEmail($ldapEntry[$attr][0]);
220
-		}
221
-		unset($attr);
222
-
223
-		// LDAP Username, needed for s2s sharing
224
-		if(isset($ldapEntry['uid'])) {
225
-			$this->storeLDAPUserName($ldapEntry['uid'][0]);
226
-		} else if(isset($ldapEntry['samaccountname'])) {
227
-			$this->storeLDAPUserName($ldapEntry['samaccountname'][0]);
228
-		}
229
-
230
-		//homePath
231
-		if(strpos($this->connection->homeFolderNamingRule, 'attr:') === 0) {
232
-			$attr = strtolower(substr($this->connection->homeFolderNamingRule, strlen('attr:')));
233
-			if(isset($ldapEntry[$attr])) {
234
-				$this->access->cacheUserHome(
235
-					$this->getUsername(), $this->getHomePath($ldapEntry[$attr][0]));
236
-			}
237
-		}
238
-
239
-		//memberOf groups
240
-		$cacheKey = 'getMemberOf'.$this->getUsername();
241
-		$groups = false;
242
-		if(isset($ldapEntry['memberof'])) {
243
-			$groups = $ldapEntry['memberof'];
244
-		}
245
-		$this->connection->writeToCache($cacheKey, $groups);
246
-
247
-		//Avatar
248
-		$attrs = array('jpegphoto', 'thumbnailphoto');
249
-		foreach ($attrs as $attr)  {
250
-			if(isset($ldapEntry[$attr])) {
251
-				$this->avatarImage = $ldapEntry[$attr][0];
252
-				// the call to the method that saves the avatar in the file
253
-				// system must be postponed after the login. It is to ensure
254
-				// external mounts are mounted properly (e.g. with login
255
-				// credentials from the session).
256
-				\OCP\Util::connectHook('OC_User', 'post_login', $this, 'updateAvatarPostLogin');
257
-				break;
258
-			}
259
-		}
260
-	}
261
-
262
-	/**
263
-	 * @brief returns the LDAP DN of the user
264
-	 * @return string
265
-	 */
266
-	public function getDN() {
267
-		return $this->dn;
268
-	}
269
-
270
-	/**
271
-	 * @brief returns the Nextcloud internal username of the user
272
-	 * @return string
273
-	 */
274
-	public function getUsername() {
275
-		return $this->uid;
276
-	}
277
-
278
-	/**
279
-	 * returns the home directory of the user if specified by LDAP settings
280
-	 * @param string $valueFromLDAP
281
-	 * @return bool|string
282
-	 * @throws \Exception
283
-	 */
284
-	public function getHomePath($valueFromLDAP = null) {
285
-		$path = (string)$valueFromLDAP;
286
-		$attr = null;
287
-
288
-		if (is_null($valueFromLDAP)
289
-		   && strpos($this->access->connection->homeFolderNamingRule, 'attr:') === 0
290
-		   && $this->access->connection->homeFolderNamingRule !== 'attr:')
291
-		{
292
-			$attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:'));
293
-			$homedir = $this->access->readAttribute(
294
-				$this->access->username2dn($this->getUsername()), $attr);
295
-			if ($homedir && isset($homedir[0])) {
296
-				$path = $homedir[0];
297
-			}
298
-		}
299
-
300
-		if ($path !== '') {
301
-			//if attribute's value is an absolute path take this, otherwise append it to data dir
302
-			//check for / at the beginning or pattern c:\ resp. c:/
303
-			if(   '/' !== $path[0]
304
-			   && !(3 < strlen($path) && ctype_alpha($path[0])
305
-			       && $path[1] === ':' && ('\\' === $path[2] || '/' === $path[2]))
306
-			) {
307
-				$path = $this->config->getSystemValue('datadirectory',
308
-						\OC::$SERVERROOT.'/data' ) . '/' . $path;
309
-			}
310
-			//we need it to store it in the DB as well in case a user gets
311
-			//deleted so we can clean up afterwards
312
-			$this->config->setUserValue(
313
-				$this->getUsername(), 'user_ldap', 'homePath', $path
314
-			);
315
-			return $path;
316
-		}
317
-
318
-		if(    !is_null($attr)
319
-			&& $this->config->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', true)
320
-		) {
321
-			// a naming rule attribute is defined, but it doesn't exist for that LDAP user
322
-			throw new \Exception('Home dir attribute can\'t be read from LDAP for uid: ' . $this->getUsername());
323
-		}
324
-
325
-		//false will apply default behaviour as defined and done by OC_User
326
-		$this->config->setUserValue($this->getUsername(), 'user_ldap', 'homePath', '');
327
-		return false;
328
-	}
329
-
330
-	public function getMemberOfGroups() {
331
-		$cacheKey = 'getMemberOf'.$this->getUsername();
332
-		$memberOfGroups = $this->connection->getFromCache($cacheKey);
333
-		if(!is_null($memberOfGroups)) {
334
-			return $memberOfGroups;
335
-		}
336
-		$groupDNs = $this->access->readAttribute($this->getDN(), 'memberOf');
337
-		$this->connection->writeToCache($cacheKey, $groupDNs);
338
-		return $groupDNs;
339
-	}
340
-
341
-	/**
342
-	 * @brief reads the image from LDAP that shall be used as Avatar
343
-	 * @return string data (provided by LDAP) | false
344
-	 */
345
-	public function getAvatarImage() {
346
-		if(!is_null($this->avatarImage)) {
347
-			return $this->avatarImage;
348
-		}
349
-
350
-		$this->avatarImage = false;
351
-		$attributes = array('jpegPhoto', 'thumbnailPhoto');
352
-		foreach($attributes as $attribute) {
353
-			$result = $this->access->readAttribute($this->dn, $attribute);
354
-			if($result !== false && is_array($result) && isset($result[0])) {
355
-				$this->avatarImage = $result[0];
356
-				break;
357
-			}
358
-		}
359
-
360
-		return $this->avatarImage;
361
-	}
362
-
363
-	/**
364
-	 * @brief marks the user as having logged in at least once
365
-	 * @return null
366
-	 */
367
-	public function markLogin() {
368
-		$this->config->setUserValue(
369
-			$this->uid, 'user_ldap', self::USER_PREFKEY_FIRSTLOGIN, 1);
370
-	}
371
-
372
-	/**
373
-	 * @brief marks the time when user features like email have been updated
374
-	 * @return null
375
-	 */
376
-	public function markRefreshTime() {
377
-		$this->config->setUserValue(
378
-			$this->uid, 'user_ldap', self::USER_PREFKEY_LASTREFRESH, time());
379
-	}
380
-
381
-	/**
382
-	 * @brief checks whether user features needs to be updated again by
383
-	 * comparing the difference of time of the last refresh to now with the
384
-	 * desired interval
385
-	 * @return bool
386
-	 */
387
-	private function needsRefresh() {
388
-		$lastChecked = $this->config->getUserValue($this->uid, 'user_ldap',
389
-			self::USER_PREFKEY_LASTREFRESH, 0);
390
-
391
-		if((time() - (int)$lastChecked) < (int)$this->config->getAppValue('user_ldap', 'updateAttributesInterval', 86400)) {
392
-			return false;
393
-		}
394
-		return  true;
395
-	}
396
-
397
-	/**
398
-	 * Stores a key-value pair in relation to this user
399
-	 *
400
-	 * @param string $key
401
-	 * @param string $value
402
-	 */
403
-	private function store($key, $value) {
404
-		$this->config->setUserValue($this->uid, 'user_ldap', $key, $value);
405
-	}
406
-
407
-	/**
408
-	 * Composes the display name and stores it in the database. The final
409
-	 * display name is returned.
410
-	 *
411
-	 * @param string $displayName
412
-	 * @param string $displayName2
413
-	 * @returns string the effective display name
414
-	 */
415
-	public function composeAndStoreDisplayName($displayName, $displayName2 = '') {
416
-		$displayName2 = (string)$displayName2;
417
-		if($displayName2 !== '') {
418
-			$displayName .= ' (' . $displayName2 . ')';
419
-		}
420
-		$this->store('displayName', $displayName);
421
-		return $displayName;
422
-	}
423
-
424
-	/**
425
-	 * Stores the LDAP Username in the Database
426
-	 * @param string $userName
427
-	 */
428
-	public function storeLDAPUserName($userName) {
429
-		$this->store('uid', $userName);
430
-	}
431
-
432
-	/**
433
-	 * @brief checks whether an update method specified by feature was run
434
-	 * already. If not, it will marked like this, because it is expected that
435
-	 * the method will be run, when false is returned.
436
-	 * @param string $feature email | quota | avatar (can be extended)
437
-	 * @return bool
438
-	 */
439
-	private function wasRefreshed($feature) {
440
-		if(isset($this->refreshedFeatures[$feature])) {
441
-			return true;
442
-		}
443
-		$this->refreshedFeatures[$feature] = 1;
444
-		return false;
445
-	}
446
-
447
-	/**
448
-	 * fetches the email from LDAP and stores it as Nextcloud user value
449
-	 * @param string $valueFromLDAP if known, to save an LDAP read request
450
-	 * @return null
451
-	 */
452
-	public function updateEmail($valueFromLDAP = null) {
453
-		if($this->wasRefreshed('email')) {
454
-			return;
455
-		}
456
-		$email = (string)$valueFromLDAP;
457
-		if(is_null($valueFromLDAP)) {
458
-			$emailAttribute = $this->connection->ldapEmailAttribute;
459
-			if ($emailAttribute !== '') {
460
-				$aEmail = $this->access->readAttribute($this->dn, $emailAttribute);
461
-				if(is_array($aEmail) && (count($aEmail) > 0)) {
462
-					$email = (string)$aEmail[0];
463
-				}
464
-			}
465
-		}
466
-		if ($email !== '') {
467
-			$user = $this->userManager->get($this->uid);
468
-			if (!is_null($user)) {
469
-				$currentEmail = (string)$user->getEMailAddress();
470
-				if ($currentEmail !== $email) {
471
-					$user->setEMailAddress($email);
472
-				}
473
-			}
474
-		}
475
-	}
476
-
477
-	/**
478
-	 * Overall process goes as follow:
479
-	 * 1. fetch the quota from LDAP and check if it's parseable with the "verifyQuotaValue" function
480
-	 * 2. if the value can't be fetched, is empty or not parseable, use the default LDAP quota
481
-	 * 3. if the default LDAP quota can't be parsed, use the Nextcloud's default quota (use 'default')
482
-	 * 4. check if the target user exists and set the quota for the user.
483
-	 *
484
-	 * In order to improve performance and prevent an unwanted extra LDAP call, the $valueFromLDAP
485
-	 * parameter can be passed with the value of the attribute. This value will be considered as the
486
-	 * quota for the user coming from the LDAP server (step 1 of the process) It can be useful to
487
-	 * fetch all the user's attributes in one call and use the fetched values in this function.
488
-	 * The expected value for that parameter is a string describing the quota for the user. Valid
489
-	 * values are 'none' (unlimited), 'default' (the Nextcloud's default quota), '1234' (quota in
490
-	 * bytes), '1234 MB' (quota in MB - check the \OC_Helper::computerFileSize method for more info)
491
-	 *
492
-	 * fetches the quota from LDAP and stores it as Nextcloud user value
493
-	 * @param string $valueFromLDAP the quota attribute's value can be passed,
494
-	 * to save the readAttribute request
495
-	 * @return null
496
-	 */
497
-	public function updateQuota($valueFromLDAP = null) {
498
-		if($this->wasRefreshed('quota')) {
499
-			return;
500
-		}
501
-
502
-		$quota = false;
503
-		if(is_null($valueFromLDAP)) {
504
-			$quotaAttribute = $this->connection->ldapQuotaAttribute;
505
-			if ($quotaAttribute !== '') {
506
-				$aQuota = $this->access->readAttribute($this->dn, $quotaAttribute);
507
-				if($aQuota && (count($aQuota) > 0)) {
508
-					if ($this->verifyQuotaValue($aQuota[0])) {
509
-						$quota = $aQuota[0];
510
-					} else {
511
-						$this->log->log('not suitable LDAP quota found for user ' . $this->uid . ': [' . $aQuota[0] . ']', ILogger::WARN);
512
-					}
513
-				}
514
-			}
515
-		} else {
516
-			if ($this->verifyQuotaValue($valueFromLDAP)) {
517
-				$quota = $valueFromLDAP;
518
-			} else {
519
-				$this->log->log('not suitable LDAP quota found for user ' . $this->uid . ': [' . $valueFromLDAP . ']', ILogger::WARN);
520
-			}
521
-		}
522
-
523
-		if ($quota === false) {
524
-			// quota not found using the LDAP attribute (or not parseable). Try the default quota
525
-			$defaultQuota = $this->connection->ldapQuotaDefault;
526
-			if ($this->verifyQuotaValue($defaultQuota)) {
527
-				$quota = $defaultQuota;
528
-			}
529
-		}
530
-
531
-		$targetUser = $this->userManager->get($this->uid);
532
-		if ($targetUser) {
533
-			if($quota !== false) {
534
-				$targetUser->setQuota($quota);
535
-			} else {
536
-				$this->log->log('not suitable default quota found for user ' . $this->uid . ': [' . $defaultQuota . ']', ILogger::WARN);
537
-			}
538
-		} else {
539
-			$this->log->log('trying to set a quota for user ' . $this->uid . ' but the user is missing', ILogger::ERROR);
540
-		}
541
-	}
542
-
543
-	private function verifyQuotaValue($quotaValue) {
544
-		return $quotaValue === 'none' || $quotaValue === 'default' || \OC_Helper::computerFileSize($quotaValue) !== false;
545
-	}
546
-
547
-	/**
548
-	 * called by a post_login hook to save the avatar picture
549
-	 *
550
-	 * @param array $params
551
-	 */
552
-	public function updateAvatarPostLogin($params) {
553
-		if(isset($params['uid']) && $params['uid'] === $this->getUsername()) {
554
-			$this->updateAvatar();
555
-		}
556
-	}
557
-
558
-	/**
559
-	 * @brief attempts to get an image from LDAP and sets it as Nextcloud avatar
560
-	 * @return null
561
-	 */
562
-	public function updateAvatar() {
563
-		if($this->wasRefreshed('avatar')) {
564
-			return;
565
-		}
566
-		$avatarImage = $this->getAvatarImage();
567
-		if($avatarImage === false) {
568
-			//not set, nothing left to do;
569
-			return;
570
-		}
571
-		$this->image->loadFromBase64(base64_encode($avatarImage));
572
-		$this->setOwnCloudAvatar();
573
-	}
574
-
575
-	/**
576
-	 * @brief sets an image as Nextcloud avatar
577
-	 * @return null
578
-	 */
579
-	private function setOwnCloudAvatar() {
580
-		if(!$this->image->valid()) {
581
-			$this->log->log('jpegPhoto data invalid for '.$this->dn, ILogger::ERROR);
582
-			return;
583
-		}
584
-		//make sure it is a square and not bigger than 128x128
585
-		$size = min(array($this->image->width(), $this->image->height(), 128));
586
-		if(!$this->image->centerCrop($size)) {
587
-			$this->log->log('croping image for avatar failed for '.$this->dn, ILogger::ERROR);
588
-			return;
589
-		}
590
-
591
-		if(!$this->fs->isLoaded()) {
592
-			$this->fs->setup($this->uid);
593
-		}
594
-
595
-		try {
596
-			$avatar = $this->avatarManager->getAvatar($this->uid);
597
-			$avatar->set($this->image);
598
-		} catch (\Exception $e) {
599
-			\OC::$server->getLogger()->logException($e, [
600
-				'message' => 'Could not set avatar for ' . $this->dn,
601
-				'level' => ILogger::INFO,
602
-				'app' => 'user_ldap',
603
-			]);
604
-		}
605
-	}
606
-
607
-	/**
608
-	 * called by a post_login hook to handle password expiry
609
-	 *
610
-	 * @param array $params
611
-	 */
612
-	public function handlePasswordExpiry($params) {
613
-		$ppolicyDN = $this->connection->ldapDefaultPPolicyDN;
614
-		if (empty($ppolicyDN) || ((int)$this->connection->turnOnPasswordChange !== 1)) {
615
-			return;//password expiry handling disabled
616
-		}
617
-		$uid = $params['uid'];
618
-		if(isset($uid) && $uid === $this->getUsername()) {
619
-			//retrieve relevant user attributes
620
-			$result = $this->access->search('objectclass=*', $this->dn, ['pwdpolicysubentry', 'pwdgraceusetime', 'pwdreset', 'pwdchangedtime']);
128
+        if ($username === null) {
129
+            $log->log("uid for '$dn' must not be null!", ILogger::ERROR);
130
+            throw new \InvalidArgumentException('uid must not be null!');
131
+        } else if ($username === '') {
132
+            $log->log("uid for '$dn' must not be an empty string", ILogger::ERROR);
133
+            throw new \InvalidArgumentException('uid must not be an empty string!');
134
+        }
135
+
136
+        $this->access              = $access;
137
+        $this->connection          = $access->getConnection();
138
+        $this->config              = $config;
139
+        $this->fs                  = $fs;
140
+        $this->dn                  = $dn;
141
+        $this->uid                 = $username;
142
+        $this->image               = $image;
143
+        $this->log                 = $log;
144
+        $this->avatarManager       = $avatarManager;
145
+        $this->userManager         = $userManager;
146
+        $this->notificationManager = $notificationManager;
147
+
148
+        \OCP\Util::connectHook('OC_User', 'post_login', $this, 'handlePasswordExpiry');
149
+    }
150
+
151
+    /**
152
+     * @brief updates properties like email, quota or avatar provided by LDAP
153
+     * @return null
154
+     */
155
+    public function update() {
156
+        if(is_null($this->dn)) {
157
+            return null;
158
+        }
159
+
160
+        $hasLoggedIn = $this->config->getUserValue($this->uid, 'user_ldap',
161
+                self::USER_PREFKEY_FIRSTLOGIN, 0);
162
+
163
+        if($this->needsRefresh()) {
164
+            $this->updateEmail();
165
+            $this->updateQuota();
166
+            if($hasLoggedIn !== 0) {
167
+                //we do not need to try it, when the user has not been logged in
168
+                //before, because the file system will not be ready.
169
+                $this->updateAvatar();
170
+                //in order to get an avatar as soon as possible, mark the user
171
+                //as refreshed only when updating the avatar did happen
172
+                $this->markRefreshTime();
173
+            }
174
+        }
175
+    }
176
+
177
+    /**
178
+     * processes results from LDAP for attributes as returned by getAttributesToRead()
179
+     * @param array $ldapEntry the user entry as retrieved from LDAP
180
+     */
181
+    public function processAttributes($ldapEntry) {
182
+        $this->markRefreshTime();
183
+        //Quota
184
+        $attr = strtolower($this->connection->ldapQuotaAttribute);
185
+        if(isset($ldapEntry[$attr])) {
186
+            $this->updateQuota($ldapEntry[$attr][0]);
187
+        } else {
188
+            if ($this->connection->ldapQuotaDefault !== '') {
189
+                $this->updateQuota();
190
+            }
191
+        }
192
+        unset($attr);
193
+
194
+        //displayName
195
+        $displayName = $displayName2 = '';
196
+        $attr = strtolower($this->connection->ldapUserDisplayName);
197
+        if(isset($ldapEntry[$attr])) {
198
+            $displayName = (string)$ldapEntry[$attr][0];
199
+        }
200
+        $attr = strtolower($this->connection->ldapUserDisplayName2);
201
+        if(isset($ldapEntry[$attr])) {
202
+            $displayName2 = (string)$ldapEntry[$attr][0];
203
+        }
204
+        if ($displayName !== '') {
205
+            $this->composeAndStoreDisplayName($displayName);
206
+            $this->access->cacheUserDisplayName(
207
+                $this->getUsername(),
208
+                $displayName,
209
+                $displayName2
210
+            );
211
+        }
212
+        unset($attr);
213
+
214
+        //Email
215
+        //email must be stored after displayname, because it would cause a user
216
+        //change event that will trigger fetching the display name again
217
+        $attr = strtolower($this->connection->ldapEmailAttribute);
218
+        if(isset($ldapEntry[$attr])) {
219
+            $this->updateEmail($ldapEntry[$attr][0]);
220
+        }
221
+        unset($attr);
222
+
223
+        // LDAP Username, needed for s2s sharing
224
+        if(isset($ldapEntry['uid'])) {
225
+            $this->storeLDAPUserName($ldapEntry['uid'][0]);
226
+        } else if(isset($ldapEntry['samaccountname'])) {
227
+            $this->storeLDAPUserName($ldapEntry['samaccountname'][0]);
228
+        }
229
+
230
+        //homePath
231
+        if(strpos($this->connection->homeFolderNamingRule, 'attr:') === 0) {
232
+            $attr = strtolower(substr($this->connection->homeFolderNamingRule, strlen('attr:')));
233
+            if(isset($ldapEntry[$attr])) {
234
+                $this->access->cacheUserHome(
235
+                    $this->getUsername(), $this->getHomePath($ldapEntry[$attr][0]));
236
+            }
237
+        }
238
+
239
+        //memberOf groups
240
+        $cacheKey = 'getMemberOf'.$this->getUsername();
241
+        $groups = false;
242
+        if(isset($ldapEntry['memberof'])) {
243
+            $groups = $ldapEntry['memberof'];
244
+        }
245
+        $this->connection->writeToCache($cacheKey, $groups);
246
+
247
+        //Avatar
248
+        $attrs = array('jpegphoto', 'thumbnailphoto');
249
+        foreach ($attrs as $attr)  {
250
+            if(isset($ldapEntry[$attr])) {
251
+                $this->avatarImage = $ldapEntry[$attr][0];
252
+                // the call to the method that saves the avatar in the file
253
+                // system must be postponed after the login. It is to ensure
254
+                // external mounts are mounted properly (e.g. with login
255
+                // credentials from the session).
256
+                \OCP\Util::connectHook('OC_User', 'post_login', $this, 'updateAvatarPostLogin');
257
+                break;
258
+            }
259
+        }
260
+    }
261
+
262
+    /**
263
+     * @brief returns the LDAP DN of the user
264
+     * @return string
265
+     */
266
+    public function getDN() {
267
+        return $this->dn;
268
+    }
269
+
270
+    /**
271
+     * @brief returns the Nextcloud internal username of the user
272
+     * @return string
273
+     */
274
+    public function getUsername() {
275
+        return $this->uid;
276
+    }
277
+
278
+    /**
279
+     * returns the home directory of the user if specified by LDAP settings
280
+     * @param string $valueFromLDAP
281
+     * @return bool|string
282
+     * @throws \Exception
283
+     */
284
+    public function getHomePath($valueFromLDAP = null) {
285
+        $path = (string)$valueFromLDAP;
286
+        $attr = null;
287
+
288
+        if (is_null($valueFromLDAP)
289
+           && strpos($this->access->connection->homeFolderNamingRule, 'attr:') === 0
290
+           && $this->access->connection->homeFolderNamingRule !== 'attr:')
291
+        {
292
+            $attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:'));
293
+            $homedir = $this->access->readAttribute(
294
+                $this->access->username2dn($this->getUsername()), $attr);
295
+            if ($homedir && isset($homedir[0])) {
296
+                $path = $homedir[0];
297
+            }
298
+        }
299
+
300
+        if ($path !== '') {
301
+            //if attribute's value is an absolute path take this, otherwise append it to data dir
302
+            //check for / at the beginning or pattern c:\ resp. c:/
303
+            if(   '/' !== $path[0]
304
+               && !(3 < strlen($path) && ctype_alpha($path[0])
305
+                   && $path[1] === ':' && ('\\' === $path[2] || '/' === $path[2]))
306
+            ) {
307
+                $path = $this->config->getSystemValue('datadirectory',
308
+                        \OC::$SERVERROOT.'/data' ) . '/' . $path;
309
+            }
310
+            //we need it to store it in the DB as well in case a user gets
311
+            //deleted so we can clean up afterwards
312
+            $this->config->setUserValue(
313
+                $this->getUsername(), 'user_ldap', 'homePath', $path
314
+            );
315
+            return $path;
316
+        }
317
+
318
+        if(    !is_null($attr)
319
+            && $this->config->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', true)
320
+        ) {
321
+            // a naming rule attribute is defined, but it doesn't exist for that LDAP user
322
+            throw new \Exception('Home dir attribute can\'t be read from LDAP for uid: ' . $this->getUsername());
323
+        }
324
+
325
+        //false will apply default behaviour as defined and done by OC_User
326
+        $this->config->setUserValue($this->getUsername(), 'user_ldap', 'homePath', '');
327
+        return false;
328
+    }
329
+
330
+    public function getMemberOfGroups() {
331
+        $cacheKey = 'getMemberOf'.$this->getUsername();
332
+        $memberOfGroups = $this->connection->getFromCache($cacheKey);
333
+        if(!is_null($memberOfGroups)) {
334
+            return $memberOfGroups;
335
+        }
336
+        $groupDNs = $this->access->readAttribute($this->getDN(), 'memberOf');
337
+        $this->connection->writeToCache($cacheKey, $groupDNs);
338
+        return $groupDNs;
339
+    }
340
+
341
+    /**
342
+     * @brief reads the image from LDAP that shall be used as Avatar
343
+     * @return string data (provided by LDAP) | false
344
+     */
345
+    public function getAvatarImage() {
346
+        if(!is_null($this->avatarImage)) {
347
+            return $this->avatarImage;
348
+        }
349
+
350
+        $this->avatarImage = false;
351
+        $attributes = array('jpegPhoto', 'thumbnailPhoto');
352
+        foreach($attributes as $attribute) {
353
+            $result = $this->access->readAttribute($this->dn, $attribute);
354
+            if($result !== false && is_array($result) && isset($result[0])) {
355
+                $this->avatarImage = $result[0];
356
+                break;
357
+            }
358
+        }
359
+
360
+        return $this->avatarImage;
361
+    }
362
+
363
+    /**
364
+     * @brief marks the user as having logged in at least once
365
+     * @return null
366
+     */
367
+    public function markLogin() {
368
+        $this->config->setUserValue(
369
+            $this->uid, 'user_ldap', self::USER_PREFKEY_FIRSTLOGIN, 1);
370
+    }
371
+
372
+    /**
373
+     * @brief marks the time when user features like email have been updated
374
+     * @return null
375
+     */
376
+    public function markRefreshTime() {
377
+        $this->config->setUserValue(
378
+            $this->uid, 'user_ldap', self::USER_PREFKEY_LASTREFRESH, time());
379
+    }
380
+
381
+    /**
382
+     * @brief checks whether user features needs to be updated again by
383
+     * comparing the difference of time of the last refresh to now with the
384
+     * desired interval
385
+     * @return bool
386
+     */
387
+    private function needsRefresh() {
388
+        $lastChecked = $this->config->getUserValue($this->uid, 'user_ldap',
389
+            self::USER_PREFKEY_LASTREFRESH, 0);
390
+
391
+        if((time() - (int)$lastChecked) < (int)$this->config->getAppValue('user_ldap', 'updateAttributesInterval', 86400)) {
392
+            return false;
393
+        }
394
+        return  true;
395
+    }
396
+
397
+    /**
398
+     * Stores a key-value pair in relation to this user
399
+     *
400
+     * @param string $key
401
+     * @param string $value
402
+     */
403
+    private function store($key, $value) {
404
+        $this->config->setUserValue($this->uid, 'user_ldap', $key, $value);
405
+    }
406
+
407
+    /**
408
+     * Composes the display name and stores it in the database. The final
409
+     * display name is returned.
410
+     *
411
+     * @param string $displayName
412
+     * @param string $displayName2
413
+     * @returns string the effective display name
414
+     */
415
+    public function composeAndStoreDisplayName($displayName, $displayName2 = '') {
416
+        $displayName2 = (string)$displayName2;
417
+        if($displayName2 !== '') {
418
+            $displayName .= ' (' . $displayName2 . ')';
419
+        }
420
+        $this->store('displayName', $displayName);
421
+        return $displayName;
422
+    }
423
+
424
+    /**
425
+     * Stores the LDAP Username in the Database
426
+     * @param string $userName
427
+     */
428
+    public function storeLDAPUserName($userName) {
429
+        $this->store('uid', $userName);
430
+    }
431
+
432
+    /**
433
+     * @brief checks whether an update method specified by feature was run
434
+     * already. If not, it will marked like this, because it is expected that
435
+     * the method will be run, when false is returned.
436
+     * @param string $feature email | quota | avatar (can be extended)
437
+     * @return bool
438
+     */
439
+    private function wasRefreshed($feature) {
440
+        if(isset($this->refreshedFeatures[$feature])) {
441
+            return true;
442
+        }
443
+        $this->refreshedFeatures[$feature] = 1;
444
+        return false;
445
+    }
446
+
447
+    /**
448
+     * fetches the email from LDAP and stores it as Nextcloud user value
449
+     * @param string $valueFromLDAP if known, to save an LDAP read request
450
+     * @return null
451
+     */
452
+    public function updateEmail($valueFromLDAP = null) {
453
+        if($this->wasRefreshed('email')) {
454
+            return;
455
+        }
456
+        $email = (string)$valueFromLDAP;
457
+        if(is_null($valueFromLDAP)) {
458
+            $emailAttribute = $this->connection->ldapEmailAttribute;
459
+            if ($emailAttribute !== '') {
460
+                $aEmail = $this->access->readAttribute($this->dn, $emailAttribute);
461
+                if(is_array($aEmail) && (count($aEmail) > 0)) {
462
+                    $email = (string)$aEmail[0];
463
+                }
464
+            }
465
+        }
466
+        if ($email !== '') {
467
+            $user = $this->userManager->get($this->uid);
468
+            if (!is_null($user)) {
469
+                $currentEmail = (string)$user->getEMailAddress();
470
+                if ($currentEmail !== $email) {
471
+                    $user->setEMailAddress($email);
472
+                }
473
+            }
474
+        }
475
+    }
476
+
477
+    /**
478
+     * Overall process goes as follow:
479
+     * 1. fetch the quota from LDAP and check if it's parseable with the "verifyQuotaValue" function
480
+     * 2. if the value can't be fetched, is empty or not parseable, use the default LDAP quota
481
+     * 3. if the default LDAP quota can't be parsed, use the Nextcloud's default quota (use 'default')
482
+     * 4. check if the target user exists and set the quota for the user.
483
+     *
484
+     * In order to improve performance and prevent an unwanted extra LDAP call, the $valueFromLDAP
485
+     * parameter can be passed with the value of the attribute. This value will be considered as the
486
+     * quota for the user coming from the LDAP server (step 1 of the process) It can be useful to
487
+     * fetch all the user's attributes in one call and use the fetched values in this function.
488
+     * The expected value for that parameter is a string describing the quota for the user. Valid
489
+     * values are 'none' (unlimited), 'default' (the Nextcloud's default quota), '1234' (quota in
490
+     * bytes), '1234 MB' (quota in MB - check the \OC_Helper::computerFileSize method for more info)
491
+     *
492
+     * fetches the quota from LDAP and stores it as Nextcloud user value
493
+     * @param string $valueFromLDAP the quota attribute's value can be passed,
494
+     * to save the readAttribute request
495
+     * @return null
496
+     */
497
+    public function updateQuota($valueFromLDAP = null) {
498
+        if($this->wasRefreshed('quota')) {
499
+            return;
500
+        }
501
+
502
+        $quota = false;
503
+        if(is_null($valueFromLDAP)) {
504
+            $quotaAttribute = $this->connection->ldapQuotaAttribute;
505
+            if ($quotaAttribute !== '') {
506
+                $aQuota = $this->access->readAttribute($this->dn, $quotaAttribute);
507
+                if($aQuota && (count($aQuota) > 0)) {
508
+                    if ($this->verifyQuotaValue($aQuota[0])) {
509
+                        $quota = $aQuota[0];
510
+                    } else {
511
+                        $this->log->log('not suitable LDAP quota found for user ' . $this->uid . ': [' . $aQuota[0] . ']', ILogger::WARN);
512
+                    }
513
+                }
514
+            }
515
+        } else {
516
+            if ($this->verifyQuotaValue($valueFromLDAP)) {
517
+                $quota = $valueFromLDAP;
518
+            } else {
519
+                $this->log->log('not suitable LDAP quota found for user ' . $this->uid . ': [' . $valueFromLDAP . ']', ILogger::WARN);
520
+            }
521
+        }
522
+
523
+        if ($quota === false) {
524
+            // quota not found using the LDAP attribute (or not parseable). Try the default quota
525
+            $defaultQuota = $this->connection->ldapQuotaDefault;
526
+            if ($this->verifyQuotaValue($defaultQuota)) {
527
+                $quota = $defaultQuota;
528
+            }
529
+        }
530
+
531
+        $targetUser = $this->userManager->get($this->uid);
532
+        if ($targetUser) {
533
+            if($quota !== false) {
534
+                $targetUser->setQuota($quota);
535
+            } else {
536
+                $this->log->log('not suitable default quota found for user ' . $this->uid . ': [' . $defaultQuota . ']', ILogger::WARN);
537
+            }
538
+        } else {
539
+            $this->log->log('trying to set a quota for user ' . $this->uid . ' but the user is missing', ILogger::ERROR);
540
+        }
541
+    }
542
+
543
+    private function verifyQuotaValue($quotaValue) {
544
+        return $quotaValue === 'none' || $quotaValue === 'default' || \OC_Helper::computerFileSize($quotaValue) !== false;
545
+    }
546
+
547
+    /**
548
+     * called by a post_login hook to save the avatar picture
549
+     *
550
+     * @param array $params
551
+     */
552
+    public function updateAvatarPostLogin($params) {
553
+        if(isset($params['uid']) && $params['uid'] === $this->getUsername()) {
554
+            $this->updateAvatar();
555
+        }
556
+    }
557
+
558
+    /**
559
+     * @brief attempts to get an image from LDAP and sets it as Nextcloud avatar
560
+     * @return null
561
+     */
562
+    public function updateAvatar() {
563
+        if($this->wasRefreshed('avatar')) {
564
+            return;
565
+        }
566
+        $avatarImage = $this->getAvatarImage();
567
+        if($avatarImage === false) {
568
+            //not set, nothing left to do;
569
+            return;
570
+        }
571
+        $this->image->loadFromBase64(base64_encode($avatarImage));
572
+        $this->setOwnCloudAvatar();
573
+    }
574
+
575
+    /**
576
+     * @brief sets an image as Nextcloud avatar
577
+     * @return null
578
+     */
579
+    private function setOwnCloudAvatar() {
580
+        if(!$this->image->valid()) {
581
+            $this->log->log('jpegPhoto data invalid for '.$this->dn, ILogger::ERROR);
582
+            return;
583
+        }
584
+        //make sure it is a square and not bigger than 128x128
585
+        $size = min(array($this->image->width(), $this->image->height(), 128));
586
+        if(!$this->image->centerCrop($size)) {
587
+            $this->log->log('croping image for avatar failed for '.$this->dn, ILogger::ERROR);
588
+            return;
589
+        }
590
+
591
+        if(!$this->fs->isLoaded()) {
592
+            $this->fs->setup($this->uid);
593
+        }
594
+
595
+        try {
596
+            $avatar = $this->avatarManager->getAvatar($this->uid);
597
+            $avatar->set($this->image);
598
+        } catch (\Exception $e) {
599
+            \OC::$server->getLogger()->logException($e, [
600
+                'message' => 'Could not set avatar for ' . $this->dn,
601
+                'level' => ILogger::INFO,
602
+                'app' => 'user_ldap',
603
+            ]);
604
+        }
605
+    }
606
+
607
+    /**
608
+     * called by a post_login hook to handle password expiry
609
+     *
610
+     * @param array $params
611
+     */
612
+    public function handlePasswordExpiry($params) {
613
+        $ppolicyDN = $this->connection->ldapDefaultPPolicyDN;
614
+        if (empty($ppolicyDN) || ((int)$this->connection->turnOnPasswordChange !== 1)) {
615
+            return;//password expiry handling disabled
616
+        }
617
+        $uid = $params['uid'];
618
+        if(isset($uid) && $uid === $this->getUsername()) {
619
+            //retrieve relevant user attributes
620
+            $result = $this->access->search('objectclass=*', $this->dn, ['pwdpolicysubentry', 'pwdgraceusetime', 'pwdreset', 'pwdchangedtime']);
621 621
 			
622
-			if(array_key_exists('pwdpolicysubentry', $result[0])) {
623
-				$pwdPolicySubentry = $result[0]['pwdpolicysubentry'];
624
-				if($pwdPolicySubentry && (count($pwdPolicySubentry) > 0)){
625
-					$ppolicyDN = $pwdPolicySubentry[0];//custom ppolicy DN
626
-				}
627
-			}
622
+            if(array_key_exists('pwdpolicysubentry', $result[0])) {
623
+                $pwdPolicySubentry = $result[0]['pwdpolicysubentry'];
624
+                if($pwdPolicySubentry && (count($pwdPolicySubentry) > 0)){
625
+                    $ppolicyDN = $pwdPolicySubentry[0];//custom ppolicy DN
626
+                }
627
+            }
628 628
 			
629
-			$pwdGraceUseTime = array_key_exists('pwdgraceusetime', $result[0]) ? $result[0]['pwdgraceusetime'] : null;
630
-			$pwdReset = array_key_exists('pwdreset', $result[0]) ? $result[0]['pwdreset'] : null;
631
-			$pwdChangedTime = array_key_exists('pwdchangedtime', $result[0]) ? $result[0]['pwdchangedtime'] : null;
629
+            $pwdGraceUseTime = array_key_exists('pwdgraceusetime', $result[0]) ? $result[0]['pwdgraceusetime'] : null;
630
+            $pwdReset = array_key_exists('pwdreset', $result[0]) ? $result[0]['pwdreset'] : null;
631
+            $pwdChangedTime = array_key_exists('pwdchangedtime', $result[0]) ? $result[0]['pwdchangedtime'] : null;
632 632
 			
633
-			//retrieve relevant password policy attributes
634
-			$cacheKey = 'ppolicyAttributes' . $ppolicyDN;
635
-			$result = $this->connection->getFromCache($cacheKey);
636
-			if(is_null($result)) {
637
-				$result = $this->access->search('objectclass=*', $ppolicyDN, ['pwdgraceauthnlimit', 'pwdmaxage', 'pwdexpirewarning']);
638
-				$this->connection->writeToCache($cacheKey, $result);
639
-			}
633
+            //retrieve relevant password policy attributes
634
+            $cacheKey = 'ppolicyAttributes' . $ppolicyDN;
635
+            $result = $this->connection->getFromCache($cacheKey);
636
+            if(is_null($result)) {
637
+                $result = $this->access->search('objectclass=*', $ppolicyDN, ['pwdgraceauthnlimit', 'pwdmaxage', 'pwdexpirewarning']);
638
+                $this->connection->writeToCache($cacheKey, $result);
639
+            }
640 640
 			
641
-			$pwdGraceAuthNLimit = array_key_exists('pwdgraceauthnlimit', $result[0]) ? $result[0]['pwdgraceauthnlimit'] : null;
642
-			$pwdMaxAge = array_key_exists('pwdmaxage', $result[0]) ? $result[0]['pwdmaxage'] : null;
643
-			$pwdExpireWarning = array_key_exists('pwdexpirewarning', $result[0]) ? $result[0]['pwdexpirewarning'] : null;
641
+            $pwdGraceAuthNLimit = array_key_exists('pwdgraceauthnlimit', $result[0]) ? $result[0]['pwdgraceauthnlimit'] : null;
642
+            $pwdMaxAge = array_key_exists('pwdmaxage', $result[0]) ? $result[0]['pwdmaxage'] : null;
643
+            $pwdExpireWarning = array_key_exists('pwdexpirewarning', $result[0]) ? $result[0]['pwdexpirewarning'] : null;
644 644
 			
645
-			//handle grace login
646
-			$pwdGraceUseTimeCount = count($pwdGraceUseTime);
647
-			if($pwdGraceUseTime && $pwdGraceUseTimeCount > 0) { //was this a grace login?
648
-				if($pwdGraceAuthNLimit 
649
-					&& (count($pwdGraceAuthNLimit) > 0)
650
-					&&($pwdGraceUseTimeCount < (int)$pwdGraceAuthNLimit[0])) { //at least one more grace login available?
651
-					$this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true');
652
-					header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute(
653
-					'user_ldap.renewPassword.showRenewPasswordForm', array('user' => $uid)));
654
-				} else { //no more grace login available
655
-					header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute(
656
-					'user_ldap.renewPassword.showLoginFormInvalidPassword', array('user' => $uid)));
657
-				}
658
-				exit();
659
-			}
660
-			//handle pwdReset attribute
661
-			if($pwdReset && (count($pwdReset) > 0) && $pwdReset[0] === 'TRUE') { //user must change his password
662
-				$this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true');
663
-				header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute(
664
-				'user_ldap.renewPassword.showRenewPasswordForm', array('user' => $uid)));
665
-				exit();
666
-			}
667
-			//handle password expiry warning
668
-			if($pwdChangedTime && (count($pwdChangedTime) > 0)) {
669
-				if($pwdMaxAge && (count($pwdMaxAge) > 0)
670
-					&& $pwdExpireWarning && (count($pwdExpireWarning) > 0)) {
671
-					$pwdMaxAgeInt = (int)$pwdMaxAge[0];
672
-					$pwdExpireWarningInt = (int)$pwdExpireWarning[0];
673
-					if($pwdMaxAgeInt > 0 && $pwdExpireWarningInt > 0){
674
-						$pwdChangedTimeDt = \DateTime::createFromFormat('YmdHisZ', $pwdChangedTime[0]);
675
-						$pwdChangedTimeDt->add(new \DateInterval('PT'.$pwdMaxAgeInt.'S'));
676
-						$currentDateTime = new \DateTime();
677
-						$secondsToExpiry = $pwdChangedTimeDt->getTimestamp() - $currentDateTime->getTimestamp();
678
-						if($secondsToExpiry <= $pwdExpireWarningInt) {
679
-							//remove last password expiry warning if any
680
-							$notification = $this->notificationManager->createNotification();
681
-							$notification->setApp('user_ldap')
682
-								->setUser($uid)
683
-								->setObject('pwd_exp_warn', $uid)
684
-							;
685
-							$this->notificationManager->markProcessed($notification);
686
-							//create new password expiry warning
687
-							$notification = $this->notificationManager->createNotification();
688
-							$notification->setApp('user_ldap')
689
-								->setUser($uid)
690
-								->setDateTime($currentDateTime)
691
-								->setObject('pwd_exp_warn', $uid) 
692
-								->setSubject('pwd_exp_warn_days', [(int) ceil($secondsToExpiry / 60 / 60 / 24)])
693
-							;
694
-							$this->notificationManager->notify($notification);
695
-						}
696
-					}
697
-				}
698
-			}
699
-		}
700
-	}
645
+            //handle grace login
646
+            $pwdGraceUseTimeCount = count($pwdGraceUseTime);
647
+            if($pwdGraceUseTime && $pwdGraceUseTimeCount > 0) { //was this a grace login?
648
+                if($pwdGraceAuthNLimit 
649
+                    && (count($pwdGraceAuthNLimit) > 0)
650
+                    &&($pwdGraceUseTimeCount < (int)$pwdGraceAuthNLimit[0])) { //at least one more grace login available?
651
+                    $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true');
652
+                    header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute(
653
+                    'user_ldap.renewPassword.showRenewPasswordForm', array('user' => $uid)));
654
+                } else { //no more grace login available
655
+                    header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute(
656
+                    'user_ldap.renewPassword.showLoginFormInvalidPassword', array('user' => $uid)));
657
+                }
658
+                exit();
659
+            }
660
+            //handle pwdReset attribute
661
+            if($pwdReset && (count($pwdReset) > 0) && $pwdReset[0] === 'TRUE') { //user must change his password
662
+                $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true');
663
+                header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute(
664
+                'user_ldap.renewPassword.showRenewPasswordForm', array('user' => $uid)));
665
+                exit();
666
+            }
667
+            //handle password expiry warning
668
+            if($pwdChangedTime && (count($pwdChangedTime) > 0)) {
669
+                if($pwdMaxAge && (count($pwdMaxAge) > 0)
670
+                    && $pwdExpireWarning && (count($pwdExpireWarning) > 0)) {
671
+                    $pwdMaxAgeInt = (int)$pwdMaxAge[0];
672
+                    $pwdExpireWarningInt = (int)$pwdExpireWarning[0];
673
+                    if($pwdMaxAgeInt > 0 && $pwdExpireWarningInt > 0){
674
+                        $pwdChangedTimeDt = \DateTime::createFromFormat('YmdHisZ', $pwdChangedTime[0]);
675
+                        $pwdChangedTimeDt->add(new \DateInterval('PT'.$pwdMaxAgeInt.'S'));
676
+                        $currentDateTime = new \DateTime();
677
+                        $secondsToExpiry = $pwdChangedTimeDt->getTimestamp() - $currentDateTime->getTimestamp();
678
+                        if($secondsToExpiry <= $pwdExpireWarningInt) {
679
+                            //remove last password expiry warning if any
680
+                            $notification = $this->notificationManager->createNotification();
681
+                            $notification->setApp('user_ldap')
682
+                                ->setUser($uid)
683
+                                ->setObject('pwd_exp_warn', $uid)
684
+                            ;
685
+                            $this->notificationManager->markProcessed($notification);
686
+                            //create new password expiry warning
687
+                            $notification = $this->notificationManager->createNotification();
688
+                            $notification->setApp('user_ldap')
689
+                                ->setUser($uid)
690
+                                ->setDateTime($currentDateTime)
691
+                                ->setObject('pwd_exp_warn', $uid) 
692
+                                ->setSubject('pwd_exp_warn_days', [(int) ceil($secondsToExpiry / 60 / 60 / 24)])
693
+                            ;
694
+                            $this->notificationManager->notify($notification);
695
+                        }
696
+                    }
697
+                }
698
+            }
699
+        }
700
+    }
701 701
 }
Please login to merge, or discard this patch.
apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php 1 patch
Indentation   +76 added lines, -76 removed lines patch added patch discarded remove patch
@@ -40,87 +40,87 @@
 block discarded – undo
40 40
 use Sabre\DAV\Exception\ServiceUnavailable;
41 41
 
42 42
 class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin {
43
-	protected $nonFatalExceptions = [
44
-		NotAuthenticated::class => true,
45
-		// If tokenauth can throw this exception (which is basically as
46
-		// NotAuthenticated. So not fatal.
47
-		PasswordLoginForbidden::class => true,
48
-		// basically a NotAuthenticated
49
-		InvalidSyncToken::class => true,
50
-		// the sync client uses this to find out whether files exist,
51
-		// so it is not always an error, log it as debug
52
-		NotFound::class => true,
53
-		// this one mostly happens when the same file is uploaded at
54
-		// exactly the same time from two clients, only one client
55
-		// wins, the second one gets "Precondition failed"
56
-		PreconditionFailed::class => true,
57
-		// forbidden can be expected when trying to upload to
58
-		// read-only folders for example
59
-		Forbidden::class => true,
60
-		// Happens when an external storage or federated share is temporarily
61
-		// not available
62
-		StorageNotAvailableException::class => true,
63
-		// happens if some a client uses the wrong method for a given URL
64
-		// the error message itself is visible on the client side anyways
65
-		NotImplemented::class => true,
66
-		// happens when the parent directory is not present (for example when a
67
-		// move is done to a non-existent directory)
68
-		Conflict::class => true,
69
-		// happens when a certain method is not allowed to be called
70
-		// for example creating a folder that already exists
71
-		MethodNotAllowed::class => true,
72
-	];
43
+    protected $nonFatalExceptions = [
44
+        NotAuthenticated::class => true,
45
+        // If tokenauth can throw this exception (which is basically as
46
+        // NotAuthenticated. So not fatal.
47
+        PasswordLoginForbidden::class => true,
48
+        // basically a NotAuthenticated
49
+        InvalidSyncToken::class => true,
50
+        // the sync client uses this to find out whether files exist,
51
+        // so it is not always an error, log it as debug
52
+        NotFound::class => true,
53
+        // this one mostly happens when the same file is uploaded at
54
+        // exactly the same time from two clients, only one client
55
+        // wins, the second one gets "Precondition failed"
56
+        PreconditionFailed::class => true,
57
+        // forbidden can be expected when trying to upload to
58
+        // read-only folders for example
59
+        Forbidden::class => true,
60
+        // Happens when an external storage or federated share is temporarily
61
+        // not available
62
+        StorageNotAvailableException::class => true,
63
+        // happens if some a client uses the wrong method for a given URL
64
+        // the error message itself is visible on the client side anyways
65
+        NotImplemented::class => true,
66
+        // happens when the parent directory is not present (for example when a
67
+        // move is done to a non-existent directory)
68
+        Conflict::class => true,
69
+        // happens when a certain method is not allowed to be called
70
+        // for example creating a folder that already exists
71
+        MethodNotAllowed::class => true,
72
+    ];
73 73
 
74
-	/** @var string */
75
-	private $appName;
74
+    /** @var string */
75
+    private $appName;
76 76
 
77
-	/** @var ILogger */
78
-	private $logger;
77
+    /** @var ILogger */
78
+    private $logger;
79 79
 
80
-	/**
81
-	 * @param string $loggerAppName app name to use when logging
82
-	 * @param ILogger $logger
83
-	 */
84
-	public function __construct($loggerAppName, $logger) {
85
-		$this->appName = $loggerAppName;
86
-		$this->logger = $logger;
87
-	}
80
+    /**
81
+     * @param string $loggerAppName app name to use when logging
82
+     * @param ILogger $logger
83
+     */
84
+    public function __construct($loggerAppName, $logger) {
85
+        $this->appName = $loggerAppName;
86
+        $this->logger = $logger;
87
+    }
88 88
 
89
-	/**
90
-	 * This initializes the plugin.
91
-	 *
92
-	 * This function is called by \Sabre\DAV\Server, after
93
-	 * addPlugin is called.
94
-	 *
95
-	 * This method should set up the required event subscriptions.
96
-	 *
97
-	 * @param \Sabre\DAV\Server $server
98
-	 * @return void
99
-	 */
100
-	public function initialize(\Sabre\DAV\Server $server) {
89
+    /**
90
+     * This initializes the plugin.
91
+     *
92
+     * This function is called by \Sabre\DAV\Server, after
93
+     * addPlugin is called.
94
+     *
95
+     * This method should set up the required event subscriptions.
96
+     *
97
+     * @param \Sabre\DAV\Server $server
98
+     * @return void
99
+     */
100
+    public function initialize(\Sabre\DAV\Server $server) {
101 101
 
102
-		$server->on('exception', array($this, 'logException'), 10);
103
-	}
102
+        $server->on('exception', array($this, 'logException'), 10);
103
+    }
104 104
 
105
-	/**
106
-	 * Log exception
107
-	 *
108
-	 */
109
-	public function logException(\Exception $ex) {
110
-		$exceptionClass = get_class($ex);
111
-		$level = ILogger::FATAL;
112
-		if (isset($this->nonFatalExceptions[$exceptionClass]) ||
113
-			(
114
-				$exceptionClass === ServiceUnavailable::class &&
115
-				$ex->getMessage() === 'System in maintenance mode.'
116
-			)
117
-		) {
118
-			$level = ILogger::DEBUG;
119
-		}
105
+    /**
106
+     * Log exception
107
+     *
108
+     */
109
+    public function logException(\Exception $ex) {
110
+        $exceptionClass = get_class($ex);
111
+        $level = ILogger::FATAL;
112
+        if (isset($this->nonFatalExceptions[$exceptionClass]) ||
113
+            (
114
+                $exceptionClass === ServiceUnavailable::class &&
115
+                $ex->getMessage() === 'System in maintenance mode.'
116
+            )
117
+        ) {
118
+            $level = ILogger::DEBUG;
119
+        }
120 120
 
121
-		$this->logger->logException($ex, [
122
-			'app' => $this->appName,
123
-			'level' => $level,
124
-		]);
125
-	}
121
+        $this->logger->logException($ex, [
122
+            'app' => $this->appName,
123
+            'level' => $level,
124
+        ]);
125
+    }
126 126
 }
Please login to merge, or discard this patch.
apps/federatedfilesharing/lib/FederatedShareProvider.php 1 patch
Indentation   +975 added lines, -975 removed lines patch added patch discarded remove patch
@@ -53,989 +53,989 @@
 block discarded – undo
53 53
  */
54 54
 class FederatedShareProvider implements IShareProvider {
55 55
 
56
-	const SHARE_TYPE_REMOTE = 6;
57
-
58
-	/** @var IDBConnection */
59
-	private $dbConnection;
60
-
61
-	/** @var AddressHandler */
62
-	private $addressHandler;
63
-
64
-	/** @var Notifications */
65
-	private $notifications;
66
-
67
-	/** @var TokenHandler */
68
-	private $tokenHandler;
69
-
70
-	/** @var IL10N */
71
-	private $l;
72
-
73
-	/** @var ILogger */
74
-	private $logger;
75
-
76
-	/** @var IRootFolder */
77
-	private $rootFolder;
78
-
79
-	/** @var IConfig */
80
-	private $config;
81
-
82
-	/** @var string */
83
-	private $externalShareTable = 'share_external';
84
-
85
-	/** @var IUserManager */
86
-	private $userManager;
87
-
88
-	/** @var ICloudIdManager */
89
-	private $cloudIdManager;
90
-
91
-	/** @var \OCP\GlobalScale\IConfig */
92
-	private $gsConfig;
93
-
94
-	/**
95
-	 * DefaultShareProvider constructor.
96
-	 *
97
-	 * @param IDBConnection $connection
98
-	 * @param AddressHandler $addressHandler
99
-	 * @param Notifications $notifications
100
-	 * @param TokenHandler $tokenHandler
101
-	 * @param IL10N $l10n
102
-	 * @param ILogger $logger
103
-	 * @param IRootFolder $rootFolder
104
-	 * @param IConfig $config
105
-	 * @param IUserManager $userManager
106
-	 * @param ICloudIdManager $cloudIdManager
107
-	 * @param \OCP\GlobalScale\IConfig $globalScaleConfig
108
-	 */
109
-	public function __construct(
110
-			IDBConnection $connection,
111
-			AddressHandler $addressHandler,
112
-			Notifications $notifications,
113
-			TokenHandler $tokenHandler,
114
-			IL10N $l10n,
115
-			ILogger $logger,
116
-			IRootFolder $rootFolder,
117
-			IConfig $config,
118
-			IUserManager $userManager,
119
-			ICloudIdManager $cloudIdManager,
120
-			\OCP\GlobalScale\IConfig $globalScaleConfig
121
-	) {
122
-		$this->dbConnection = $connection;
123
-		$this->addressHandler = $addressHandler;
124
-		$this->notifications = $notifications;
125
-		$this->tokenHandler = $tokenHandler;
126
-		$this->l = $l10n;
127
-		$this->logger = $logger;
128
-		$this->rootFolder = $rootFolder;
129
-		$this->config = $config;
130
-		$this->userManager = $userManager;
131
-		$this->cloudIdManager = $cloudIdManager;
132
-		$this->gsConfig = $globalScaleConfig;
133
-	}
134
-
135
-	/**
136
-	 * Return the identifier of this provider.
137
-	 *
138
-	 * @return string Containing only [a-zA-Z0-9]
139
-	 */
140
-	public function identifier() {
141
-		return 'ocFederatedSharing';
142
-	}
143
-
144
-	/**
145
-	 * Share a path
146
-	 *
147
-	 * @param IShare $share
148
-	 * @return IShare The share object
149
-	 * @throws ShareNotFound
150
-	 * @throws \Exception
151
-	 */
152
-	public function create(IShare $share) {
153
-
154
-		$shareWith = $share->getSharedWith();
155
-		$itemSource = $share->getNodeId();
156
-		$itemType = $share->getNodeType();
157
-		$permissions = $share->getPermissions();
158
-		$sharedBy = $share->getSharedBy();
159
-
160
-		/*
56
+    const SHARE_TYPE_REMOTE = 6;
57
+
58
+    /** @var IDBConnection */
59
+    private $dbConnection;
60
+
61
+    /** @var AddressHandler */
62
+    private $addressHandler;
63
+
64
+    /** @var Notifications */
65
+    private $notifications;
66
+
67
+    /** @var TokenHandler */
68
+    private $tokenHandler;
69
+
70
+    /** @var IL10N */
71
+    private $l;
72
+
73
+    /** @var ILogger */
74
+    private $logger;
75
+
76
+    /** @var IRootFolder */
77
+    private $rootFolder;
78
+
79
+    /** @var IConfig */
80
+    private $config;
81
+
82
+    /** @var string */
83
+    private $externalShareTable = 'share_external';
84
+
85
+    /** @var IUserManager */
86
+    private $userManager;
87
+
88
+    /** @var ICloudIdManager */
89
+    private $cloudIdManager;
90
+
91
+    /** @var \OCP\GlobalScale\IConfig */
92
+    private $gsConfig;
93
+
94
+    /**
95
+     * DefaultShareProvider constructor.
96
+     *
97
+     * @param IDBConnection $connection
98
+     * @param AddressHandler $addressHandler
99
+     * @param Notifications $notifications
100
+     * @param TokenHandler $tokenHandler
101
+     * @param IL10N $l10n
102
+     * @param ILogger $logger
103
+     * @param IRootFolder $rootFolder
104
+     * @param IConfig $config
105
+     * @param IUserManager $userManager
106
+     * @param ICloudIdManager $cloudIdManager
107
+     * @param \OCP\GlobalScale\IConfig $globalScaleConfig
108
+     */
109
+    public function __construct(
110
+            IDBConnection $connection,
111
+            AddressHandler $addressHandler,
112
+            Notifications $notifications,
113
+            TokenHandler $tokenHandler,
114
+            IL10N $l10n,
115
+            ILogger $logger,
116
+            IRootFolder $rootFolder,
117
+            IConfig $config,
118
+            IUserManager $userManager,
119
+            ICloudIdManager $cloudIdManager,
120
+            \OCP\GlobalScale\IConfig $globalScaleConfig
121
+    ) {
122
+        $this->dbConnection = $connection;
123
+        $this->addressHandler = $addressHandler;
124
+        $this->notifications = $notifications;
125
+        $this->tokenHandler = $tokenHandler;
126
+        $this->l = $l10n;
127
+        $this->logger = $logger;
128
+        $this->rootFolder = $rootFolder;
129
+        $this->config = $config;
130
+        $this->userManager = $userManager;
131
+        $this->cloudIdManager = $cloudIdManager;
132
+        $this->gsConfig = $globalScaleConfig;
133
+    }
134
+
135
+    /**
136
+     * Return the identifier of this provider.
137
+     *
138
+     * @return string Containing only [a-zA-Z0-9]
139
+     */
140
+    public function identifier() {
141
+        return 'ocFederatedSharing';
142
+    }
143
+
144
+    /**
145
+     * Share a path
146
+     *
147
+     * @param IShare $share
148
+     * @return IShare The share object
149
+     * @throws ShareNotFound
150
+     * @throws \Exception
151
+     */
152
+    public function create(IShare $share) {
153
+
154
+        $shareWith = $share->getSharedWith();
155
+        $itemSource = $share->getNodeId();
156
+        $itemType = $share->getNodeType();
157
+        $permissions = $share->getPermissions();
158
+        $sharedBy = $share->getSharedBy();
159
+
160
+        /*
161 161
 		 * Check if file is not already shared with the remote user
162 162
 		 */
163
-		$alreadyShared = $this->getSharedWith($shareWith, self::SHARE_TYPE_REMOTE, $share->getNode(), 1, 0);
164
-		if (!empty($alreadyShared)) {
165
-			$message = 'Sharing %s failed, because this item is already shared with %s';
166
-			$message_t = $this->l->t('Sharing %s failed, because this item is already shared with %s', array($share->getNode()->getName(), $shareWith));
167
-			$this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']);
168
-			throw new \Exception($message_t);
169
-		}
170
-
171
-
172
-		// don't allow federated shares if source and target server are the same
173
-		$cloudId = $this->cloudIdManager->resolveCloudId($shareWith);
174
-		$currentServer = $this->addressHandler->generateRemoteURL();
175
-		$currentUser = $sharedBy;
176
-		if ($this->addressHandler->compareAddresses($cloudId->getUser(), $cloudId->getRemote(), $currentUser, $currentServer)) {
177
-			$message = 'Not allowed to create a federated share with the same user.';
178
-			$message_t = $this->l->t('Not allowed to create a federated share with the same user');
179
-			$this->logger->debug($message, ['app' => 'Federated File Sharing']);
180
-			throw new \Exception($message_t);
181
-		}
182
-
183
-
184
-		$share->setSharedWith($cloudId->getId());
185
-
186
-		try {
187
-			$remoteShare = $this->getShareFromExternalShareTable($share);
188
-		} catch (ShareNotFound $e) {
189
-			$remoteShare = null;
190
-		}
191
-
192
-		if ($remoteShare) {
193
-			try {
194
-				$ownerCloudId = $this->cloudIdManager->getCloudId($remoteShare['owner'], $remoteShare['remote']);
195
-				$shareId = $this->addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $ownerCloudId->getId(), $permissions, 'tmp_token_' . time());
196
-				$share->setId($shareId);
197
-				list($token, $remoteId) = $this->askOwnerToReShare($shareWith, $share, $shareId);
198
-				// remote share was create successfully if we get a valid token as return
199
-				$send = is_string($token) && $token !== '';
200
-			} catch (\Exception $e) {
201
-				// fall back to old re-share behavior if the remote server
202
-				// doesn't support flat re-shares (was introduced with Nextcloud 9.1)
203
-				$this->removeShareFromTable($share);
204
-				$shareId = $this->createFederatedShare($share);
205
-			}
206
-			if ($send) {
207
-				$this->updateSuccessfulReshare($shareId, $token);
208
-				$this->storeRemoteId($shareId, $remoteId);
209
-			} else {
210
-				$this->removeShareFromTable($share);
211
-				$message_t = $this->l->t('File is already shared with %s', [$shareWith]);
212
-				throw new \Exception($message_t);
213
-			}
214
-
215
-		} else {
216
-			$shareId = $this->createFederatedShare($share);
217
-		}
218
-
219
-		$data = $this->getRawShare($shareId);
220
-		return $this->createShareObject($data);
221
-	}
222
-
223
-	/**
224
-	 * create federated share and inform the recipient
225
-	 *
226
-	 * @param IShare $share
227
-	 * @return int
228
-	 * @throws ShareNotFound
229
-	 * @throws \Exception
230
-	 */
231
-	protected function createFederatedShare(IShare $share) {
232
-		$token = $this->tokenHandler->generateToken();
233
-		$shareId = $this->addShareToDB(
234
-			$share->getNodeId(),
235
-			$share->getNodeType(),
236
-			$share->getSharedWith(),
237
-			$share->getSharedBy(),
238
-			$share->getShareOwner(),
239
-			$share->getPermissions(),
240
-			$token
241
-		);
242
-
243
-		$failure = false;
244
-
245
-		try {
246
-			$sharedByFederatedId = $share->getSharedBy();
247
-			if ($this->userManager->userExists($sharedByFederatedId)) {
248
-				$cloudId = $this->cloudIdManager->getCloudId($sharedByFederatedId, $this->addressHandler->generateRemoteURL());
249
-				$sharedByFederatedId = $cloudId->getId();
250
-			}
251
-			$ownerCloudId = $this->cloudIdManager->getCloudId($share->getShareOwner(), $this->addressHandler->generateRemoteURL());
252
-			$send = $this->notifications->sendRemoteShare(
253
-				$token,
254
-				$share->getSharedWith(),
255
-				$share->getNode()->getName(),
256
-				$shareId,
257
-				$share->getShareOwner(),
258
-				$ownerCloudId->getId(),
259
-				$share->getSharedBy(),
260
-				$sharedByFederatedId
261
-			);
262
-
263
-			if ($send === false) {
264
-				$failure = true;
265
-			}
266
-		} catch (\Exception $e) {
267
-			$this->logger->logException($e, [
268
-				'message' => 'Failed to notify remote server of federated share, removing share.',
269
-				'level' => ILogger::ERROR,
270
-				'app' => 'federatedfilesharing',
271
-			]);
272
-			$failure = true;
273
-		}
274
-
275
-		if($failure) {
276
-			$this->removeShareFromTableById($shareId);
277
-			$message_t = $this->l->t('Sharing %s failed, could not find %s, maybe the server is currently unreachable or uses a self-signed certificate.',
278
-				[$share->getNode()->getName(), $share->getSharedWith()]);
279
-			throw new \Exception($message_t);
280
-		}
281
-
282
-		return $shareId;
283
-
284
-	}
285
-
286
-	/**
287
-	 * @param string $shareWith
288
-	 * @param IShare $share
289
-	 * @param string $shareId internal share Id
290
-	 * @return array
291
-	 * @throws \Exception
292
-	 */
293
-	protected function askOwnerToReShare($shareWith, IShare $share, $shareId) {
294
-
295
-		$remoteShare = $this->getShareFromExternalShareTable($share);
296
-		$token = $remoteShare['share_token'];
297
-		$remoteId = $remoteShare['remote_id'];
298
-		$remote = $remoteShare['remote'];
299
-
300
-		list($token, $remoteId) = $this->notifications->requestReShare(
301
-			$token,
302
-			$remoteId,
303
-			$shareId,
304
-			$remote,
305
-			$shareWith,
306
-			$share->getPermissions()
307
-		);
308
-
309
-		return [$token, $remoteId];
310
-	}
311
-
312
-	/**
313
-	 * get federated share from the share_external table but exclude mounted link shares
314
-	 *
315
-	 * @param IShare $share
316
-	 * @return array
317
-	 * @throws ShareNotFound
318
-	 */
319
-	protected function getShareFromExternalShareTable(IShare $share) {
320
-		$query = $this->dbConnection->getQueryBuilder();
321
-		$query->select('*')->from($this->externalShareTable)
322
-			->where($query->expr()->eq('user', $query->createNamedParameter($share->getShareOwner())))
323
-			->andWhere($query->expr()->eq('mountpoint', $query->createNamedParameter($share->getTarget())));
324
-		$result = $query->execute()->fetchAll();
325
-
326
-		if (isset($result[0]) && (int)$result[0]['remote_id'] > 0) {
327
-			return $result[0];
328
-		}
329
-
330
-		throw new ShareNotFound('share not found in share_external table');
331
-	}
332
-
333
-	/**
334
-	 * add share to the database and return the ID
335
-	 *
336
-	 * @param int $itemSource
337
-	 * @param string $itemType
338
-	 * @param string $shareWith
339
-	 * @param string $sharedBy
340
-	 * @param string $uidOwner
341
-	 * @param int $permissions
342
-	 * @param string $token
343
-	 * @return int
344
-	 */
345
-	private function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token) {
346
-		$qb = $this->dbConnection->getQueryBuilder();
347
-		$qb->insert('share')
348
-			->setValue('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))
349
-			->setValue('item_type', $qb->createNamedParameter($itemType))
350
-			->setValue('item_source', $qb->createNamedParameter($itemSource))
351
-			->setValue('file_source', $qb->createNamedParameter($itemSource))
352
-			->setValue('share_with', $qb->createNamedParameter($shareWith))
353
-			->setValue('uid_owner', $qb->createNamedParameter($uidOwner))
354
-			->setValue('uid_initiator', $qb->createNamedParameter($sharedBy))
355
-			->setValue('permissions', $qb->createNamedParameter($permissions))
356
-			->setValue('token', $qb->createNamedParameter($token))
357
-			->setValue('stime', $qb->createNamedParameter(time()));
358
-
359
-		/*
163
+        $alreadyShared = $this->getSharedWith($shareWith, self::SHARE_TYPE_REMOTE, $share->getNode(), 1, 0);
164
+        if (!empty($alreadyShared)) {
165
+            $message = 'Sharing %s failed, because this item is already shared with %s';
166
+            $message_t = $this->l->t('Sharing %s failed, because this item is already shared with %s', array($share->getNode()->getName(), $shareWith));
167
+            $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']);
168
+            throw new \Exception($message_t);
169
+        }
170
+
171
+
172
+        // don't allow federated shares if source and target server are the same
173
+        $cloudId = $this->cloudIdManager->resolveCloudId($shareWith);
174
+        $currentServer = $this->addressHandler->generateRemoteURL();
175
+        $currentUser = $sharedBy;
176
+        if ($this->addressHandler->compareAddresses($cloudId->getUser(), $cloudId->getRemote(), $currentUser, $currentServer)) {
177
+            $message = 'Not allowed to create a federated share with the same user.';
178
+            $message_t = $this->l->t('Not allowed to create a federated share with the same user');
179
+            $this->logger->debug($message, ['app' => 'Federated File Sharing']);
180
+            throw new \Exception($message_t);
181
+        }
182
+
183
+
184
+        $share->setSharedWith($cloudId->getId());
185
+
186
+        try {
187
+            $remoteShare = $this->getShareFromExternalShareTable($share);
188
+        } catch (ShareNotFound $e) {
189
+            $remoteShare = null;
190
+        }
191
+
192
+        if ($remoteShare) {
193
+            try {
194
+                $ownerCloudId = $this->cloudIdManager->getCloudId($remoteShare['owner'], $remoteShare['remote']);
195
+                $shareId = $this->addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $ownerCloudId->getId(), $permissions, 'tmp_token_' . time());
196
+                $share->setId($shareId);
197
+                list($token, $remoteId) = $this->askOwnerToReShare($shareWith, $share, $shareId);
198
+                // remote share was create successfully if we get a valid token as return
199
+                $send = is_string($token) && $token !== '';
200
+            } catch (\Exception $e) {
201
+                // fall back to old re-share behavior if the remote server
202
+                // doesn't support flat re-shares (was introduced with Nextcloud 9.1)
203
+                $this->removeShareFromTable($share);
204
+                $shareId = $this->createFederatedShare($share);
205
+            }
206
+            if ($send) {
207
+                $this->updateSuccessfulReshare($shareId, $token);
208
+                $this->storeRemoteId($shareId, $remoteId);
209
+            } else {
210
+                $this->removeShareFromTable($share);
211
+                $message_t = $this->l->t('File is already shared with %s', [$shareWith]);
212
+                throw new \Exception($message_t);
213
+            }
214
+
215
+        } else {
216
+            $shareId = $this->createFederatedShare($share);
217
+        }
218
+
219
+        $data = $this->getRawShare($shareId);
220
+        return $this->createShareObject($data);
221
+    }
222
+
223
+    /**
224
+     * create federated share and inform the recipient
225
+     *
226
+     * @param IShare $share
227
+     * @return int
228
+     * @throws ShareNotFound
229
+     * @throws \Exception
230
+     */
231
+    protected function createFederatedShare(IShare $share) {
232
+        $token = $this->tokenHandler->generateToken();
233
+        $shareId = $this->addShareToDB(
234
+            $share->getNodeId(),
235
+            $share->getNodeType(),
236
+            $share->getSharedWith(),
237
+            $share->getSharedBy(),
238
+            $share->getShareOwner(),
239
+            $share->getPermissions(),
240
+            $token
241
+        );
242
+
243
+        $failure = false;
244
+
245
+        try {
246
+            $sharedByFederatedId = $share->getSharedBy();
247
+            if ($this->userManager->userExists($sharedByFederatedId)) {
248
+                $cloudId = $this->cloudIdManager->getCloudId($sharedByFederatedId, $this->addressHandler->generateRemoteURL());
249
+                $sharedByFederatedId = $cloudId->getId();
250
+            }
251
+            $ownerCloudId = $this->cloudIdManager->getCloudId($share->getShareOwner(), $this->addressHandler->generateRemoteURL());
252
+            $send = $this->notifications->sendRemoteShare(
253
+                $token,
254
+                $share->getSharedWith(),
255
+                $share->getNode()->getName(),
256
+                $shareId,
257
+                $share->getShareOwner(),
258
+                $ownerCloudId->getId(),
259
+                $share->getSharedBy(),
260
+                $sharedByFederatedId
261
+            );
262
+
263
+            if ($send === false) {
264
+                $failure = true;
265
+            }
266
+        } catch (\Exception $e) {
267
+            $this->logger->logException($e, [
268
+                'message' => 'Failed to notify remote server of federated share, removing share.',
269
+                'level' => ILogger::ERROR,
270
+                'app' => 'federatedfilesharing',
271
+            ]);
272
+            $failure = true;
273
+        }
274
+
275
+        if($failure) {
276
+            $this->removeShareFromTableById($shareId);
277
+            $message_t = $this->l->t('Sharing %s failed, could not find %s, maybe the server is currently unreachable or uses a self-signed certificate.',
278
+                [$share->getNode()->getName(), $share->getSharedWith()]);
279
+            throw new \Exception($message_t);
280
+        }
281
+
282
+        return $shareId;
283
+
284
+    }
285
+
286
+    /**
287
+     * @param string $shareWith
288
+     * @param IShare $share
289
+     * @param string $shareId internal share Id
290
+     * @return array
291
+     * @throws \Exception
292
+     */
293
+    protected function askOwnerToReShare($shareWith, IShare $share, $shareId) {
294
+
295
+        $remoteShare = $this->getShareFromExternalShareTable($share);
296
+        $token = $remoteShare['share_token'];
297
+        $remoteId = $remoteShare['remote_id'];
298
+        $remote = $remoteShare['remote'];
299
+
300
+        list($token, $remoteId) = $this->notifications->requestReShare(
301
+            $token,
302
+            $remoteId,
303
+            $shareId,
304
+            $remote,
305
+            $shareWith,
306
+            $share->getPermissions()
307
+        );
308
+
309
+        return [$token, $remoteId];
310
+    }
311
+
312
+    /**
313
+     * get federated share from the share_external table but exclude mounted link shares
314
+     *
315
+     * @param IShare $share
316
+     * @return array
317
+     * @throws ShareNotFound
318
+     */
319
+    protected function getShareFromExternalShareTable(IShare $share) {
320
+        $query = $this->dbConnection->getQueryBuilder();
321
+        $query->select('*')->from($this->externalShareTable)
322
+            ->where($query->expr()->eq('user', $query->createNamedParameter($share->getShareOwner())))
323
+            ->andWhere($query->expr()->eq('mountpoint', $query->createNamedParameter($share->getTarget())));
324
+        $result = $query->execute()->fetchAll();
325
+
326
+        if (isset($result[0]) && (int)$result[0]['remote_id'] > 0) {
327
+            return $result[0];
328
+        }
329
+
330
+        throw new ShareNotFound('share not found in share_external table');
331
+    }
332
+
333
+    /**
334
+     * add share to the database and return the ID
335
+     *
336
+     * @param int $itemSource
337
+     * @param string $itemType
338
+     * @param string $shareWith
339
+     * @param string $sharedBy
340
+     * @param string $uidOwner
341
+     * @param int $permissions
342
+     * @param string $token
343
+     * @return int
344
+     */
345
+    private function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token) {
346
+        $qb = $this->dbConnection->getQueryBuilder();
347
+        $qb->insert('share')
348
+            ->setValue('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))
349
+            ->setValue('item_type', $qb->createNamedParameter($itemType))
350
+            ->setValue('item_source', $qb->createNamedParameter($itemSource))
351
+            ->setValue('file_source', $qb->createNamedParameter($itemSource))
352
+            ->setValue('share_with', $qb->createNamedParameter($shareWith))
353
+            ->setValue('uid_owner', $qb->createNamedParameter($uidOwner))
354
+            ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy))
355
+            ->setValue('permissions', $qb->createNamedParameter($permissions))
356
+            ->setValue('token', $qb->createNamedParameter($token))
357
+            ->setValue('stime', $qb->createNamedParameter(time()));
358
+
359
+        /*
360 360
 		 * Added to fix https://github.com/owncloud/core/issues/22215
361 361
 		 * Can be removed once we get rid of ajax/share.php
362 362
 		 */
363
-		$qb->setValue('file_target', $qb->createNamedParameter(''));
364
-
365
-		$qb->execute();
366
-		$id = $qb->getLastInsertId();
367
-
368
-		return (int)$id;
369
-	}
370
-
371
-	/**
372
-	 * Update a share
373
-	 *
374
-	 * @param IShare $share
375
-	 * @return IShare The share object
376
-	 */
377
-	public function update(IShare $share) {
378
-		/*
363
+        $qb->setValue('file_target', $qb->createNamedParameter(''));
364
+
365
+        $qb->execute();
366
+        $id = $qb->getLastInsertId();
367
+
368
+        return (int)$id;
369
+    }
370
+
371
+    /**
372
+     * Update a share
373
+     *
374
+     * @param IShare $share
375
+     * @return IShare The share object
376
+     */
377
+    public function update(IShare $share) {
378
+        /*
379 379
 		 * We allow updating the permissions of federated shares
380 380
 		 */
381
-		$qb = $this->dbConnection->getQueryBuilder();
382
-			$qb->update('share')
383
-				->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId())))
384
-				->set('permissions', $qb->createNamedParameter($share->getPermissions()))
385
-				->set('uid_owner', $qb->createNamedParameter($share->getShareOwner()))
386
-				->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy()))
387
-				->execute();
388
-
389
-		// send the updated permission to the owner/initiator, if they are not the same
390
-		if ($share->getShareOwner() !== $share->getSharedBy()) {
391
-			$this->sendPermissionUpdate($share);
392
-		}
393
-
394
-		return $share;
395
-	}
396
-
397
-	/**
398
-	 * send the updated permission to the owner/initiator, if they are not the same
399
-	 *
400
-	 * @param IShare $share
401
-	 * @throws ShareNotFound
402
-	 * @throws \OC\HintException
403
-	 */
404
-	protected function sendPermissionUpdate(IShare $share) {
405
-		$remoteId = $this->getRemoteId($share);
406
-		// if the local user is the owner we send the permission change to the initiator
407
-		if ($this->userManager->userExists($share->getShareOwner())) {
408
-			list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
409
-		} else { // ... if not we send the permission change to the owner
410
-			list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner());
411
-		}
412
-		$this->notifications->sendPermissionChange($remote, $remoteId, $share->getToken(), $share->getPermissions());
413
-	}
414
-
415
-
416
-	/**
417
-	 * update successful reShare with the correct token
418
-	 *
419
-	 * @param int $shareId
420
-	 * @param string $token
421
-	 */
422
-	protected function updateSuccessfulReShare($shareId, $token) {
423
-		$query = $this->dbConnection->getQueryBuilder();
424
-		$query->update('share')
425
-			->where($query->expr()->eq('id', $query->createNamedParameter($shareId)))
426
-			->set('token', $query->createNamedParameter($token))
427
-			->execute();
428
-	}
429
-
430
-	/**
431
-	 * store remote ID in federated reShare table
432
-	 *
433
-	 * @param $shareId
434
-	 * @param $remoteId
435
-	 */
436
-	public function storeRemoteId($shareId, $remoteId) {
437
-		$query = $this->dbConnection->getQueryBuilder();
438
-		$query->insert('federated_reshares')
439
-			->values(
440
-				[
441
-					'share_id' =>  $query->createNamedParameter($shareId),
442
-					'remote_id' => $query->createNamedParameter($remoteId),
443
-				]
444
-			);
445
-		$query->execute();
446
-	}
447
-
448
-	/**
449
-	 * get share ID on remote server for federated re-shares
450
-	 *
451
-	 * @param IShare $share
452
-	 * @return int
453
-	 * @throws ShareNotFound
454
-	 */
455
-	public function getRemoteId(IShare $share) {
456
-		$query = $this->dbConnection->getQueryBuilder();
457
-		$query->select('remote_id')->from('federated_reshares')
458
-			->where($query->expr()->eq('share_id', $query->createNamedParameter((int)$share->getId())));
459
-		$data = $query->execute()->fetch();
460
-
461
-		if (!is_array($data) || !isset($data['remote_id'])) {
462
-			throw new ShareNotFound();
463
-		}
464
-
465
-		return (int)$data['remote_id'];
466
-	}
467
-
468
-	/**
469
-	 * @inheritdoc
470
-	 */
471
-	public function move(IShare $share, $recipient) {
472
-		/*
381
+        $qb = $this->dbConnection->getQueryBuilder();
382
+            $qb->update('share')
383
+                ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId())))
384
+                ->set('permissions', $qb->createNamedParameter($share->getPermissions()))
385
+                ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner()))
386
+                ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy()))
387
+                ->execute();
388
+
389
+        // send the updated permission to the owner/initiator, if they are not the same
390
+        if ($share->getShareOwner() !== $share->getSharedBy()) {
391
+            $this->sendPermissionUpdate($share);
392
+        }
393
+
394
+        return $share;
395
+    }
396
+
397
+    /**
398
+     * send the updated permission to the owner/initiator, if they are not the same
399
+     *
400
+     * @param IShare $share
401
+     * @throws ShareNotFound
402
+     * @throws \OC\HintException
403
+     */
404
+    protected function sendPermissionUpdate(IShare $share) {
405
+        $remoteId = $this->getRemoteId($share);
406
+        // if the local user is the owner we send the permission change to the initiator
407
+        if ($this->userManager->userExists($share->getShareOwner())) {
408
+            list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
409
+        } else { // ... if not we send the permission change to the owner
410
+            list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner());
411
+        }
412
+        $this->notifications->sendPermissionChange($remote, $remoteId, $share->getToken(), $share->getPermissions());
413
+    }
414
+
415
+
416
+    /**
417
+     * update successful reShare with the correct token
418
+     *
419
+     * @param int $shareId
420
+     * @param string $token
421
+     */
422
+    protected function updateSuccessfulReShare($shareId, $token) {
423
+        $query = $this->dbConnection->getQueryBuilder();
424
+        $query->update('share')
425
+            ->where($query->expr()->eq('id', $query->createNamedParameter($shareId)))
426
+            ->set('token', $query->createNamedParameter($token))
427
+            ->execute();
428
+    }
429
+
430
+    /**
431
+     * store remote ID in federated reShare table
432
+     *
433
+     * @param $shareId
434
+     * @param $remoteId
435
+     */
436
+    public function storeRemoteId($shareId, $remoteId) {
437
+        $query = $this->dbConnection->getQueryBuilder();
438
+        $query->insert('federated_reshares')
439
+            ->values(
440
+                [
441
+                    'share_id' =>  $query->createNamedParameter($shareId),
442
+                    'remote_id' => $query->createNamedParameter($remoteId),
443
+                ]
444
+            );
445
+        $query->execute();
446
+    }
447
+
448
+    /**
449
+     * get share ID on remote server for federated re-shares
450
+     *
451
+     * @param IShare $share
452
+     * @return int
453
+     * @throws ShareNotFound
454
+     */
455
+    public function getRemoteId(IShare $share) {
456
+        $query = $this->dbConnection->getQueryBuilder();
457
+        $query->select('remote_id')->from('federated_reshares')
458
+            ->where($query->expr()->eq('share_id', $query->createNamedParameter((int)$share->getId())));
459
+        $data = $query->execute()->fetch();
460
+
461
+        if (!is_array($data) || !isset($data['remote_id'])) {
462
+            throw new ShareNotFound();
463
+        }
464
+
465
+        return (int)$data['remote_id'];
466
+    }
467
+
468
+    /**
469
+     * @inheritdoc
470
+     */
471
+    public function move(IShare $share, $recipient) {
472
+        /*
473 473
 		 * This function does nothing yet as it is just for outgoing
474 474
 		 * federated shares.
475 475
 		 */
476
-		return $share;
477
-	}
478
-
479
-	/**
480
-	 * Get all children of this share
481
-	 *
482
-	 * @param IShare $parent
483
-	 * @return IShare[]
484
-	 */
485
-	public function getChildren(IShare $parent) {
486
-		$children = [];
487
-
488
-		$qb = $this->dbConnection->getQueryBuilder();
489
-		$qb->select('*')
490
-			->from('share')
491
-			->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId())))
492
-			->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)))
493
-			->orderBy('id');
494
-
495
-		$cursor = $qb->execute();
496
-		while($data = $cursor->fetch()) {
497
-			$children[] = $this->createShareObject($data);
498
-		}
499
-		$cursor->closeCursor();
500
-
501
-		return $children;
502
-	}
503
-
504
-	/**
505
-	 * Delete a share (owner unShares the file)
506
-	 *
507
-	 * @param IShare $share
508
-	 */
509
-	public function delete(IShare $share) {
510
-
511
-		list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedWith());
512
-
513
-		$isOwner = false;
514
-
515
-		$this->removeShareFromTable($share);
516
-
517
-		// if the local user is the owner we can send the unShare request directly...
518
-		if ($this->userManager->userExists($share->getShareOwner())) {
519
-			$this->notifications->sendRemoteUnShare($remote, $share->getId(), $share->getToken());
520
-			$this->revokeShare($share, true);
521
-			$isOwner = true;
522
-		} else { // ... if not we need to correct ID for the unShare request
523
-			$remoteId = $this->getRemoteId($share);
524
-			$this->notifications->sendRemoteUnShare($remote, $remoteId, $share->getToken());
525
-			$this->revokeShare($share, false);
526
-		}
527
-
528
-		// send revoke notification to the other user, if initiator and owner are not the same user
529
-		if ($share->getShareOwner() !== $share->getSharedBy()) {
530
-			$remoteId = $this->getRemoteId($share);
531
-			if ($isOwner) {
532
-				list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
533
-			} else {
534
-				list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner());
535
-			}
536
-			$this->notifications->sendRevokeShare($remote, $remoteId, $share->getToken());
537
-		}
538
-	}
539
-
540
-	/**
541
-	 * in case of a re-share we need to send the other use (initiator or owner)
542
-	 * a message that the file was unshared
543
-	 *
544
-	 * @param IShare $share
545
-	 * @param bool $isOwner the user can either be the owner or the user who re-sahred it
546
-	 * @throws ShareNotFound
547
-	 * @throws \OC\HintException
548
-	 */
549
-	protected function revokeShare($share, $isOwner) {
550
-		// also send a unShare request to the initiator, if this is a different user than the owner
551
-		if ($share->getShareOwner() !== $share->getSharedBy()) {
552
-			if ($isOwner) {
553
-				list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
554
-			} else {
555
-				list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner());
556
-			}
557
-			$remoteId = $this->getRemoteId($share);
558
-			$this->notifications->sendRevokeShare($remote, $remoteId, $share->getToken());
559
-		}
560
-	}
561
-
562
-	/**
563
-	 * remove share from table
564
-	 *
565
-	 * @param IShare $share
566
-	 */
567
-	public function removeShareFromTable(IShare $share) {
568
-		$this->removeShareFromTableById($share->getId());
569
-	}
570
-
571
-	/**
572
-	 * remove share from table
573
-	 *
574
-	 * @param string $shareId
575
-	 */
576
-	private function removeShareFromTableById($shareId) {
577
-		$qb = $this->dbConnection->getQueryBuilder();
578
-		$qb->delete('share')
579
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId)));
580
-		$qb->execute();
581
-
582
-		$qb->delete('federated_reshares')
583
-			->where($qb->expr()->eq('share_id', $qb->createNamedParameter($shareId)));
584
-		$qb->execute();
585
-	}
586
-
587
-	/**
588
-	 * @inheritdoc
589
-	 */
590
-	public function deleteFromSelf(IShare $share, $recipient) {
591
-		// nothing to do here. Technically deleteFromSelf in the context of federated
592
-		// shares is a umount of a external storage. This is handled here
593
-		// apps/files_sharing/lib/external/manager.php
594
-		// TODO move this code over to this app
595
-	}
596
-
597
-
598
-	public function getSharesInFolder($userId, Folder $node, $reshares) {
599
-		$qb = $this->dbConnection->getQueryBuilder();
600
-		$qb->select('*')
601
-			->from('share', 's')
602
-			->andWhere($qb->expr()->orX(
603
-				$qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
604
-				$qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
605
-			))
606
-			->andWhere(
607
-				$qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE))
608
-			);
609
-
610
-		/**
611
-		 * Reshares for this user are shares where they are the owner.
612
-		 */
613
-		if ($reshares === false) {
614
-			$qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)));
615
-		} else {
616
-			$qb->andWhere(
617
-				$qb->expr()->orX(
618
-					$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
619
-					$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
620
-				)
621
-			);
622
-		}
623
-
624
-		$qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
625
-		$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
626
-
627
-		$qb->orderBy('id');
628
-
629
-		$cursor = $qb->execute();
630
-		$shares = [];
631
-		while ($data = $cursor->fetch()) {
632
-			$shares[$data['fileid']][] = $this->createShareObject($data);
633
-		}
634
-		$cursor->closeCursor();
635
-
636
-		return $shares;
637
-	}
638
-
639
-	/**
640
-	 * @inheritdoc
641
-	 */
642
-	public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) {
643
-		$qb = $this->dbConnection->getQueryBuilder();
644
-		$qb->select('*')
645
-			->from('share');
646
-
647
-		$qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)));
648
-
649
-		/**
650
-		 * Reshares for this user are shares where they are the owner.
651
-		 */
652
-		if ($reshares === false) {
653
-			//Special case for old shares created via the web UI
654
-			$or1 = $qb->expr()->andX(
655
-				$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
656
-				$qb->expr()->isNull('uid_initiator')
657
-			);
658
-
659
-			$qb->andWhere(
660
-				$qb->expr()->orX(
661
-					$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)),
662
-					$or1
663
-				)
664
-			);
665
-		} else {
666
-			$qb->andWhere(
667
-				$qb->expr()->orX(
668
-					$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
669
-					$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
670
-				)
671
-			);
672
-		}
673
-
674
-		if ($node !== null) {
675
-			$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
676
-		}
677
-
678
-		if ($limit !== -1) {
679
-			$qb->setMaxResults($limit);
680
-		}
681
-
682
-		$qb->setFirstResult($offset);
683
-		$qb->orderBy('id');
684
-
685
-		$cursor = $qb->execute();
686
-		$shares = [];
687
-		while($data = $cursor->fetch()) {
688
-			$shares[] = $this->createShareObject($data);
689
-		}
690
-		$cursor->closeCursor();
691
-
692
-		return $shares;
693
-	}
694
-
695
-	/**
696
-	 * @inheritdoc
697
-	 */
698
-	public function getShareById($id, $recipientId = null) {
699
-		$qb = $this->dbConnection->getQueryBuilder();
700
-
701
-		$qb->select('*')
702
-			->from('share')
703
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
704
-			->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)));
705
-
706
-		$cursor = $qb->execute();
707
-		$data = $cursor->fetch();
708
-		$cursor->closeCursor();
709
-
710
-		if ($data === false) {
711
-			throw new ShareNotFound();
712
-		}
713
-
714
-		try {
715
-			$share = $this->createShareObject($data);
716
-		} catch (InvalidShare $e) {
717
-			throw new ShareNotFound();
718
-		}
719
-
720
-		return $share;
721
-	}
722
-
723
-	/**
724
-	 * Get shares for a given path
725
-	 *
726
-	 * @param \OCP\Files\Node $path
727
-	 * @return IShare[]
728
-	 */
729
-	public function getSharesByPath(Node $path) {
730
-		$qb = $this->dbConnection->getQueryBuilder();
731
-
732
-		$cursor = $qb->select('*')
733
-			->from('share')
734
-			->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId())))
735
-			->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)))
736
-			->execute();
737
-
738
-		$shares = [];
739
-		while($data = $cursor->fetch()) {
740
-			$shares[] = $this->createShareObject($data);
741
-		}
742
-		$cursor->closeCursor();
743
-
744
-		return $shares;
745
-	}
746
-
747
-	/**
748
-	 * @inheritdoc
749
-	 */
750
-	public function getSharedWith($userId, $shareType, $node, $limit, $offset) {
751
-		/** @var IShare[] $shares */
752
-		$shares = [];
753
-
754
-		//Get shares directly with this user
755
-		$qb = $this->dbConnection->getQueryBuilder();
756
-		$qb->select('*')
757
-			->from('share');
758
-
759
-		// Order by id
760
-		$qb->orderBy('id');
761
-
762
-		// Set limit and offset
763
-		if ($limit !== -1) {
764
-			$qb->setMaxResults($limit);
765
-		}
766
-		$qb->setFirstResult($offset);
767
-
768
-		$qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)));
769
-		$qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId)));
770
-
771
-		// Filter by node if provided
772
-		if ($node !== null) {
773
-			$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
774
-		}
775
-
776
-		$cursor = $qb->execute();
777
-
778
-		while($data = $cursor->fetch()) {
779
-			$shares[] = $this->createShareObject($data);
780
-		}
781
-		$cursor->closeCursor();
782
-
783
-
784
-		return $shares;
785
-	}
786
-
787
-	/**
788
-	 * Get a share by token
789
-	 *
790
-	 * @param string $token
791
-	 * @return IShare
792
-	 * @throws ShareNotFound
793
-	 */
794
-	public function getShareByToken($token) {
795
-		$qb = $this->dbConnection->getQueryBuilder();
796
-
797
-		$cursor = $qb->select('*')
798
-			->from('share')
799
-			->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)))
800
-			->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)))
801
-			->execute();
802
-
803
-		$data = $cursor->fetch();
804
-
805
-		if ($data === false) {
806
-			throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
807
-		}
808
-
809
-		try {
810
-			$share = $this->createShareObject($data);
811
-		} catch (InvalidShare $e) {
812
-			throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
813
-		}
814
-
815
-		return $share;
816
-	}
817
-
818
-	/**
819
-	 * get database row of a give share
820
-	 *
821
-	 * @param $id
822
-	 * @return array
823
-	 * @throws ShareNotFound
824
-	 */
825
-	private function getRawShare($id) {
826
-
827
-		// Now fetch the inserted share and create a complete share object
828
-		$qb = $this->dbConnection->getQueryBuilder();
829
-		$qb->select('*')
830
-			->from('share')
831
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
832
-
833
-		$cursor = $qb->execute();
834
-		$data = $cursor->fetch();
835
-		$cursor->closeCursor();
836
-
837
-		if ($data === false) {
838
-			throw new ShareNotFound;
839
-		}
840
-
841
-		return $data;
842
-	}
843
-
844
-	/**
845
-	 * Create a share object from an database row
846
-	 *
847
-	 * @param array $data
848
-	 * @return IShare
849
-	 * @throws InvalidShare
850
-	 * @throws ShareNotFound
851
-	 */
852
-	private function createShareObject($data) {
853
-
854
-		$share = new Share($this->rootFolder, $this->userManager);
855
-		$share->setId((int)$data['id'])
856
-			->setShareType((int)$data['share_type'])
857
-			->setPermissions((int)$data['permissions'])
858
-			->setTarget($data['file_target'])
859
-			->setMailSend((bool)$data['mail_send'])
860
-			->setToken($data['token']);
861
-
862
-		$shareTime = new \DateTime();
863
-		$shareTime->setTimestamp((int)$data['stime']);
864
-		$share->setShareTime($shareTime);
865
-		$share->setSharedWith($data['share_with']);
866
-
867
-		if ($data['uid_initiator'] !== null) {
868
-			$share->setShareOwner($data['uid_owner']);
869
-			$share->setSharedBy($data['uid_initiator']);
870
-		} else {
871
-			//OLD SHARE
872
-			$share->setSharedBy($data['uid_owner']);
873
-			$path = $this->getNode($share->getSharedBy(), (int)$data['file_source']);
874
-
875
-			$owner = $path->getOwner();
876
-			$share->setShareOwner($owner->getUID());
877
-		}
878
-
879
-		$share->setNodeId((int)$data['file_source']);
880
-		$share->setNodeType($data['item_type']);
881
-
882
-		$share->setProviderId($this->identifier());
883
-
884
-		return $share;
885
-	}
886
-
887
-	/**
888
-	 * Get the node with file $id for $user
889
-	 *
890
-	 * @param string $userId
891
-	 * @param int $id
892
-	 * @return \OCP\Files\File|\OCP\Files\Folder
893
-	 * @throws InvalidShare
894
-	 */
895
-	private function getNode($userId, $id) {
896
-		try {
897
-			$userFolder = $this->rootFolder->getUserFolder($userId);
898
-		} catch (NotFoundException $e) {
899
-			throw new InvalidShare();
900
-		}
901
-
902
-		$nodes = $userFolder->getById($id);
903
-
904
-		if (empty($nodes)) {
905
-			throw new InvalidShare();
906
-		}
907
-
908
-		return $nodes[0];
909
-	}
910
-
911
-	/**
912
-	 * A user is deleted from the system
913
-	 * So clean up the relevant shares.
914
-	 *
915
-	 * @param string $uid
916
-	 * @param int $shareType
917
-	 */
918
-	public function userDeleted($uid, $shareType) {
919
-		//TODO: probabaly a good idea to send unshare info to remote servers
920
-
921
-		$qb = $this->dbConnection->getQueryBuilder();
922
-
923
-		$qb->delete('share')
924
-			->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE)))
925
-			->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid)))
926
-			->execute();
927
-	}
928
-
929
-	/**
930
-	 * This provider does not handle groups
931
-	 *
932
-	 * @param string $gid
933
-	 */
934
-	public function groupDeleted($gid) {
935
-		// We don't handle groups here
936
-	}
937
-
938
-	/**
939
-	 * This provider does not handle groups
940
-	 *
941
-	 * @param string $uid
942
-	 * @param string $gid
943
-	 */
944
-	public function userDeletedFromGroup($uid, $gid) {
945
-		// We don't handle groups here
946
-	}
947
-
948
-	/**
949
-	 * check if users from other Nextcloud instances are allowed to mount public links share by this instance
950
-	 *
951
-	 * @return bool
952
-	 */
953
-	public function isOutgoingServer2serverShareEnabled() {
954
-		if ($this->gsConfig->onlyInternalFederation()) {
955
-			return false;
956
-		}
957
-		$result = $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes');
958
-		return ($result === 'yes');
959
-	}
960
-
961
-	/**
962
-	 * check if users are allowed to mount public links from other Nextclouds
963
-	 *
964
-	 * @return bool
965
-	 */
966
-	public function isIncomingServer2serverShareEnabled() {
967
-		if ($this->gsConfig->onlyInternalFederation()) {
968
-			return false;
969
-		}
970
-		$result = $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes');
971
-		return ($result === 'yes');
972
-	}
973
-
974
-	/**
975
-	 * Check if querying sharees on the lookup server is enabled
976
-	 *
977
-	 * @return bool
978
-	 */
979
-	public function isLookupServerQueriesEnabled() {
980
-		// in a global scale setup we should always query the lookup server
981
-		if ($this->gsConfig->isGlobalScaleEnabled()) {
982
-			return true;
983
-		}
984
-		$result = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'no');
985
-		return ($result === 'yes');
986
-	}
987
-
988
-
989
-	/**
990
-	 * Check if it is allowed to publish user specific data to the lookup server
991
-	 *
992
-	 * @return bool
993
-	 */
994
-	public function isLookupServerUploadEnabled() {
995
-		// in a global scale setup the admin is responsible to keep the lookup server up-to-date
996
-		if ($this->gsConfig->isGlobalScaleEnabled()) {
997
-			return false;
998
-		}
999
-		$result = $this->config->getAppValue('files_sharing', 'lookupServerUploadEnabled', 'yes');
1000
-		return ($result === 'yes');
1001
-	}
1002
-
1003
-	/**
1004
-	 * @inheritdoc
1005
-	 */
1006
-	public function getAccessList($nodes, $currentAccess) {
1007
-		$ids = [];
1008
-		foreach ($nodes as $node) {
1009
-			$ids[] = $node->getId();
1010
-		}
1011
-
1012
-		$qb = $this->dbConnection->getQueryBuilder();
1013
-		$qb->select('share_with', 'token', 'file_source')
1014
-			->from('share')
1015
-			->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE)))
1016
-			->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)))
1017
-			->andWhere($qb->expr()->orX(
1018
-				$qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
1019
-				$qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
1020
-			));
1021
-		$cursor = $qb->execute();
1022
-
1023
-		if ($currentAccess === false) {
1024
-			$remote = $cursor->fetch() !== false;
1025
-			$cursor->closeCursor();
1026
-
1027
-			return ['remote' => $remote];
1028
-		}
1029
-
1030
-		$remote = [];
1031
-		while ($row = $cursor->fetch()) {
1032
-			$remote[$row['share_with']] = [
1033
-				'node_id' => $row['file_source'],
1034
-				'token' => $row['token'],
1035
-			];
1036
-		}
1037
-		$cursor->closeCursor();
1038
-
1039
-		return ['remote' => $remote];
1040
-	}
476
+        return $share;
477
+    }
478
+
479
+    /**
480
+     * Get all children of this share
481
+     *
482
+     * @param IShare $parent
483
+     * @return IShare[]
484
+     */
485
+    public function getChildren(IShare $parent) {
486
+        $children = [];
487
+
488
+        $qb = $this->dbConnection->getQueryBuilder();
489
+        $qb->select('*')
490
+            ->from('share')
491
+            ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId())))
492
+            ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)))
493
+            ->orderBy('id');
494
+
495
+        $cursor = $qb->execute();
496
+        while($data = $cursor->fetch()) {
497
+            $children[] = $this->createShareObject($data);
498
+        }
499
+        $cursor->closeCursor();
500
+
501
+        return $children;
502
+    }
503
+
504
+    /**
505
+     * Delete a share (owner unShares the file)
506
+     *
507
+     * @param IShare $share
508
+     */
509
+    public function delete(IShare $share) {
510
+
511
+        list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedWith());
512
+
513
+        $isOwner = false;
514
+
515
+        $this->removeShareFromTable($share);
516
+
517
+        // if the local user is the owner we can send the unShare request directly...
518
+        if ($this->userManager->userExists($share->getShareOwner())) {
519
+            $this->notifications->sendRemoteUnShare($remote, $share->getId(), $share->getToken());
520
+            $this->revokeShare($share, true);
521
+            $isOwner = true;
522
+        } else { // ... if not we need to correct ID for the unShare request
523
+            $remoteId = $this->getRemoteId($share);
524
+            $this->notifications->sendRemoteUnShare($remote, $remoteId, $share->getToken());
525
+            $this->revokeShare($share, false);
526
+        }
527
+
528
+        // send revoke notification to the other user, if initiator and owner are not the same user
529
+        if ($share->getShareOwner() !== $share->getSharedBy()) {
530
+            $remoteId = $this->getRemoteId($share);
531
+            if ($isOwner) {
532
+                list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
533
+            } else {
534
+                list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner());
535
+            }
536
+            $this->notifications->sendRevokeShare($remote, $remoteId, $share->getToken());
537
+        }
538
+    }
539
+
540
+    /**
541
+     * in case of a re-share we need to send the other use (initiator or owner)
542
+     * a message that the file was unshared
543
+     *
544
+     * @param IShare $share
545
+     * @param bool $isOwner the user can either be the owner or the user who re-sahred it
546
+     * @throws ShareNotFound
547
+     * @throws \OC\HintException
548
+     */
549
+    protected function revokeShare($share, $isOwner) {
550
+        // also send a unShare request to the initiator, if this is a different user than the owner
551
+        if ($share->getShareOwner() !== $share->getSharedBy()) {
552
+            if ($isOwner) {
553
+                list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
554
+            } else {
555
+                list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner());
556
+            }
557
+            $remoteId = $this->getRemoteId($share);
558
+            $this->notifications->sendRevokeShare($remote, $remoteId, $share->getToken());
559
+        }
560
+    }
561
+
562
+    /**
563
+     * remove share from table
564
+     *
565
+     * @param IShare $share
566
+     */
567
+    public function removeShareFromTable(IShare $share) {
568
+        $this->removeShareFromTableById($share->getId());
569
+    }
570
+
571
+    /**
572
+     * remove share from table
573
+     *
574
+     * @param string $shareId
575
+     */
576
+    private function removeShareFromTableById($shareId) {
577
+        $qb = $this->dbConnection->getQueryBuilder();
578
+        $qb->delete('share')
579
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId)));
580
+        $qb->execute();
581
+
582
+        $qb->delete('federated_reshares')
583
+            ->where($qb->expr()->eq('share_id', $qb->createNamedParameter($shareId)));
584
+        $qb->execute();
585
+    }
586
+
587
+    /**
588
+     * @inheritdoc
589
+     */
590
+    public function deleteFromSelf(IShare $share, $recipient) {
591
+        // nothing to do here. Technically deleteFromSelf in the context of federated
592
+        // shares is a umount of a external storage. This is handled here
593
+        // apps/files_sharing/lib/external/manager.php
594
+        // TODO move this code over to this app
595
+    }
596
+
597
+
598
+    public function getSharesInFolder($userId, Folder $node, $reshares) {
599
+        $qb = $this->dbConnection->getQueryBuilder();
600
+        $qb->select('*')
601
+            ->from('share', 's')
602
+            ->andWhere($qb->expr()->orX(
603
+                $qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
604
+                $qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
605
+            ))
606
+            ->andWhere(
607
+                $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE))
608
+            );
609
+
610
+        /**
611
+         * Reshares for this user are shares where they are the owner.
612
+         */
613
+        if ($reshares === false) {
614
+            $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)));
615
+        } else {
616
+            $qb->andWhere(
617
+                $qb->expr()->orX(
618
+                    $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
619
+                    $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
620
+                )
621
+            );
622
+        }
623
+
624
+        $qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
625
+        $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
626
+
627
+        $qb->orderBy('id');
628
+
629
+        $cursor = $qb->execute();
630
+        $shares = [];
631
+        while ($data = $cursor->fetch()) {
632
+            $shares[$data['fileid']][] = $this->createShareObject($data);
633
+        }
634
+        $cursor->closeCursor();
635
+
636
+        return $shares;
637
+    }
638
+
639
+    /**
640
+     * @inheritdoc
641
+     */
642
+    public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) {
643
+        $qb = $this->dbConnection->getQueryBuilder();
644
+        $qb->select('*')
645
+            ->from('share');
646
+
647
+        $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)));
648
+
649
+        /**
650
+         * Reshares for this user are shares where they are the owner.
651
+         */
652
+        if ($reshares === false) {
653
+            //Special case for old shares created via the web UI
654
+            $or1 = $qb->expr()->andX(
655
+                $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
656
+                $qb->expr()->isNull('uid_initiator')
657
+            );
658
+
659
+            $qb->andWhere(
660
+                $qb->expr()->orX(
661
+                    $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)),
662
+                    $or1
663
+                )
664
+            );
665
+        } else {
666
+            $qb->andWhere(
667
+                $qb->expr()->orX(
668
+                    $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
669
+                    $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
670
+                )
671
+            );
672
+        }
673
+
674
+        if ($node !== null) {
675
+            $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
676
+        }
677
+
678
+        if ($limit !== -1) {
679
+            $qb->setMaxResults($limit);
680
+        }
681
+
682
+        $qb->setFirstResult($offset);
683
+        $qb->orderBy('id');
684
+
685
+        $cursor = $qb->execute();
686
+        $shares = [];
687
+        while($data = $cursor->fetch()) {
688
+            $shares[] = $this->createShareObject($data);
689
+        }
690
+        $cursor->closeCursor();
691
+
692
+        return $shares;
693
+    }
694
+
695
+    /**
696
+     * @inheritdoc
697
+     */
698
+    public function getShareById($id, $recipientId = null) {
699
+        $qb = $this->dbConnection->getQueryBuilder();
700
+
701
+        $qb->select('*')
702
+            ->from('share')
703
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
704
+            ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)));
705
+
706
+        $cursor = $qb->execute();
707
+        $data = $cursor->fetch();
708
+        $cursor->closeCursor();
709
+
710
+        if ($data === false) {
711
+            throw new ShareNotFound();
712
+        }
713
+
714
+        try {
715
+            $share = $this->createShareObject($data);
716
+        } catch (InvalidShare $e) {
717
+            throw new ShareNotFound();
718
+        }
719
+
720
+        return $share;
721
+    }
722
+
723
+    /**
724
+     * Get shares for a given path
725
+     *
726
+     * @param \OCP\Files\Node $path
727
+     * @return IShare[]
728
+     */
729
+    public function getSharesByPath(Node $path) {
730
+        $qb = $this->dbConnection->getQueryBuilder();
731
+
732
+        $cursor = $qb->select('*')
733
+            ->from('share')
734
+            ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId())))
735
+            ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)))
736
+            ->execute();
737
+
738
+        $shares = [];
739
+        while($data = $cursor->fetch()) {
740
+            $shares[] = $this->createShareObject($data);
741
+        }
742
+        $cursor->closeCursor();
743
+
744
+        return $shares;
745
+    }
746
+
747
+    /**
748
+     * @inheritdoc
749
+     */
750
+    public function getSharedWith($userId, $shareType, $node, $limit, $offset) {
751
+        /** @var IShare[] $shares */
752
+        $shares = [];
753
+
754
+        //Get shares directly with this user
755
+        $qb = $this->dbConnection->getQueryBuilder();
756
+        $qb->select('*')
757
+            ->from('share');
758
+
759
+        // Order by id
760
+        $qb->orderBy('id');
761
+
762
+        // Set limit and offset
763
+        if ($limit !== -1) {
764
+            $qb->setMaxResults($limit);
765
+        }
766
+        $qb->setFirstResult($offset);
767
+
768
+        $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)));
769
+        $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId)));
770
+
771
+        // Filter by node if provided
772
+        if ($node !== null) {
773
+            $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
774
+        }
775
+
776
+        $cursor = $qb->execute();
777
+
778
+        while($data = $cursor->fetch()) {
779
+            $shares[] = $this->createShareObject($data);
780
+        }
781
+        $cursor->closeCursor();
782
+
783
+
784
+        return $shares;
785
+    }
786
+
787
+    /**
788
+     * Get a share by token
789
+     *
790
+     * @param string $token
791
+     * @return IShare
792
+     * @throws ShareNotFound
793
+     */
794
+    public function getShareByToken($token) {
795
+        $qb = $this->dbConnection->getQueryBuilder();
796
+
797
+        $cursor = $qb->select('*')
798
+            ->from('share')
799
+            ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)))
800
+            ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)))
801
+            ->execute();
802
+
803
+        $data = $cursor->fetch();
804
+
805
+        if ($data === false) {
806
+            throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
807
+        }
808
+
809
+        try {
810
+            $share = $this->createShareObject($data);
811
+        } catch (InvalidShare $e) {
812
+            throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
813
+        }
814
+
815
+        return $share;
816
+    }
817
+
818
+    /**
819
+     * get database row of a give share
820
+     *
821
+     * @param $id
822
+     * @return array
823
+     * @throws ShareNotFound
824
+     */
825
+    private function getRawShare($id) {
826
+
827
+        // Now fetch the inserted share and create a complete share object
828
+        $qb = $this->dbConnection->getQueryBuilder();
829
+        $qb->select('*')
830
+            ->from('share')
831
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
832
+
833
+        $cursor = $qb->execute();
834
+        $data = $cursor->fetch();
835
+        $cursor->closeCursor();
836
+
837
+        if ($data === false) {
838
+            throw new ShareNotFound;
839
+        }
840
+
841
+        return $data;
842
+    }
843
+
844
+    /**
845
+     * Create a share object from an database row
846
+     *
847
+     * @param array $data
848
+     * @return IShare
849
+     * @throws InvalidShare
850
+     * @throws ShareNotFound
851
+     */
852
+    private function createShareObject($data) {
853
+
854
+        $share = new Share($this->rootFolder, $this->userManager);
855
+        $share->setId((int)$data['id'])
856
+            ->setShareType((int)$data['share_type'])
857
+            ->setPermissions((int)$data['permissions'])
858
+            ->setTarget($data['file_target'])
859
+            ->setMailSend((bool)$data['mail_send'])
860
+            ->setToken($data['token']);
861
+
862
+        $shareTime = new \DateTime();
863
+        $shareTime->setTimestamp((int)$data['stime']);
864
+        $share->setShareTime($shareTime);
865
+        $share->setSharedWith($data['share_with']);
866
+
867
+        if ($data['uid_initiator'] !== null) {
868
+            $share->setShareOwner($data['uid_owner']);
869
+            $share->setSharedBy($data['uid_initiator']);
870
+        } else {
871
+            //OLD SHARE
872
+            $share->setSharedBy($data['uid_owner']);
873
+            $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']);
874
+
875
+            $owner = $path->getOwner();
876
+            $share->setShareOwner($owner->getUID());
877
+        }
878
+
879
+        $share->setNodeId((int)$data['file_source']);
880
+        $share->setNodeType($data['item_type']);
881
+
882
+        $share->setProviderId($this->identifier());
883
+
884
+        return $share;
885
+    }
886
+
887
+    /**
888
+     * Get the node with file $id for $user
889
+     *
890
+     * @param string $userId
891
+     * @param int $id
892
+     * @return \OCP\Files\File|\OCP\Files\Folder
893
+     * @throws InvalidShare
894
+     */
895
+    private function getNode($userId, $id) {
896
+        try {
897
+            $userFolder = $this->rootFolder->getUserFolder($userId);
898
+        } catch (NotFoundException $e) {
899
+            throw new InvalidShare();
900
+        }
901
+
902
+        $nodes = $userFolder->getById($id);
903
+
904
+        if (empty($nodes)) {
905
+            throw new InvalidShare();
906
+        }
907
+
908
+        return $nodes[0];
909
+    }
910
+
911
+    /**
912
+     * A user is deleted from the system
913
+     * So clean up the relevant shares.
914
+     *
915
+     * @param string $uid
916
+     * @param int $shareType
917
+     */
918
+    public function userDeleted($uid, $shareType) {
919
+        //TODO: probabaly a good idea to send unshare info to remote servers
920
+
921
+        $qb = $this->dbConnection->getQueryBuilder();
922
+
923
+        $qb->delete('share')
924
+            ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE)))
925
+            ->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid)))
926
+            ->execute();
927
+    }
928
+
929
+    /**
930
+     * This provider does not handle groups
931
+     *
932
+     * @param string $gid
933
+     */
934
+    public function groupDeleted($gid) {
935
+        // We don't handle groups here
936
+    }
937
+
938
+    /**
939
+     * This provider does not handle groups
940
+     *
941
+     * @param string $uid
942
+     * @param string $gid
943
+     */
944
+    public function userDeletedFromGroup($uid, $gid) {
945
+        // We don't handle groups here
946
+    }
947
+
948
+    /**
949
+     * check if users from other Nextcloud instances are allowed to mount public links share by this instance
950
+     *
951
+     * @return bool
952
+     */
953
+    public function isOutgoingServer2serverShareEnabled() {
954
+        if ($this->gsConfig->onlyInternalFederation()) {
955
+            return false;
956
+        }
957
+        $result = $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes');
958
+        return ($result === 'yes');
959
+    }
960
+
961
+    /**
962
+     * check if users are allowed to mount public links from other Nextclouds
963
+     *
964
+     * @return bool
965
+     */
966
+    public function isIncomingServer2serverShareEnabled() {
967
+        if ($this->gsConfig->onlyInternalFederation()) {
968
+            return false;
969
+        }
970
+        $result = $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes');
971
+        return ($result === 'yes');
972
+    }
973
+
974
+    /**
975
+     * Check if querying sharees on the lookup server is enabled
976
+     *
977
+     * @return bool
978
+     */
979
+    public function isLookupServerQueriesEnabled() {
980
+        // in a global scale setup we should always query the lookup server
981
+        if ($this->gsConfig->isGlobalScaleEnabled()) {
982
+            return true;
983
+        }
984
+        $result = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'no');
985
+        return ($result === 'yes');
986
+    }
987
+
988
+
989
+    /**
990
+     * Check if it is allowed to publish user specific data to the lookup server
991
+     *
992
+     * @return bool
993
+     */
994
+    public function isLookupServerUploadEnabled() {
995
+        // in a global scale setup the admin is responsible to keep the lookup server up-to-date
996
+        if ($this->gsConfig->isGlobalScaleEnabled()) {
997
+            return false;
998
+        }
999
+        $result = $this->config->getAppValue('files_sharing', 'lookupServerUploadEnabled', 'yes');
1000
+        return ($result === 'yes');
1001
+    }
1002
+
1003
+    /**
1004
+     * @inheritdoc
1005
+     */
1006
+    public function getAccessList($nodes, $currentAccess) {
1007
+        $ids = [];
1008
+        foreach ($nodes as $node) {
1009
+            $ids[] = $node->getId();
1010
+        }
1011
+
1012
+        $qb = $this->dbConnection->getQueryBuilder();
1013
+        $qb->select('share_with', 'token', 'file_source')
1014
+            ->from('share')
1015
+            ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE)))
1016
+            ->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)))
1017
+            ->andWhere($qb->expr()->orX(
1018
+                $qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
1019
+                $qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
1020
+            ));
1021
+        $cursor = $qb->execute();
1022
+
1023
+        if ($currentAccess === false) {
1024
+            $remote = $cursor->fetch() !== false;
1025
+            $cursor->closeCursor();
1026
+
1027
+            return ['remote' => $remote];
1028
+        }
1029
+
1030
+        $remote = [];
1031
+        while ($row = $cursor->fetch()) {
1032
+            $remote[$row['share_with']] = [
1033
+                'node_id' => $row['file_source'],
1034
+                'token' => $row['token'],
1035
+            ];
1036
+        }
1037
+        $cursor->closeCursor();
1038
+
1039
+        return ['remote' => $remote];
1040
+    }
1041 1041
 }
Please login to merge, or discard this patch.
apps/federatedfilesharing/lib/Controller/RequestHandlerController.php 1 patch
Indentation   +635 added lines, -635 removed lines patch added patch discarded remove patch
@@ -51,639 +51,639 @@
 block discarded – undo
51 51
 
52 52
 class RequestHandlerController extends OCSController {
53 53
 
54
-	/** @var FederatedShareProvider */
55
-	private $federatedShareProvider;
56
-
57
-	/** @var IDBConnection */
58
-	private $connection;
59
-
60
-	/** @var Share\IManager */
61
-	private $shareManager;
62
-
63
-	/** @var Notifications */
64
-	private $notifications;
65
-
66
-	/** @var AddressHandler */
67
-	private $addressHandler;
68
-
69
-	/** @var  IUserManager */
70
-	private $userManager;
71
-
72
-	/** @var string */
73
-	private $shareTable = 'share';
74
-
75
-	/** @var ICloudIdManager */
76
-	private $cloudIdManager;
77
-
78
-	/** @var ILogger */
79
-	private $logger;
80
-
81
-	/**
82
-	 * Server2Server constructor.
83
-	 *
84
-	 * @param string $appName
85
-	 * @param IRequest $request
86
-	 * @param FederatedShareProvider $federatedShareProvider
87
-	 * @param IDBConnection $connection
88
-	 * @param Share\IManager $shareManager
89
-	 * @param Notifications $notifications
90
-	 * @param AddressHandler $addressHandler
91
-	 * @param IUserManager $userManager
92
-	 * @param ICloudIdManager $cloudIdManager
93
-	 */
94
-	public function __construct($appName,
95
-								IRequest $request,
96
-								FederatedShareProvider $federatedShareProvider,
97
-								IDBConnection $connection,
98
-								Share\IManager $shareManager,
99
-								Notifications $notifications,
100
-								AddressHandler $addressHandler,
101
-								IUserManager $userManager,
102
-								ICloudIdManager $cloudIdManager,
103
-								ILogger $logger
104
-	) {
105
-		parent::__construct($appName, $request);
106
-
107
-		$this->federatedShareProvider = $federatedShareProvider;
108
-		$this->connection = $connection;
109
-		$this->shareManager = $shareManager;
110
-		$this->notifications = $notifications;
111
-		$this->addressHandler = $addressHandler;
112
-		$this->userManager = $userManager;
113
-		$this->cloudIdManager = $cloudIdManager;
114
-		$this->logger = $logger;
115
-	}
116
-
117
-	/**
118
-	 * @NoCSRFRequired
119
-	 * @PublicPage
120
-	 *
121
-	 * create a new share
122
-	 *
123
-	 * @return Http\DataResponse
124
-	 * @throws OCSException
125
-	 */
126
-	public function createShare() {
127
-
128
-		if (!$this->isS2SEnabled(true)) {
129
-			throw new OCSException('Server does not support federated cloud sharing', 503);
130
-		}
131
-
132
-		$remote = isset($_POST['remote']) ? $_POST['remote'] : null;
133
-		$token = isset($_POST['token']) ? $_POST['token'] : null;
134
-		$name = isset($_POST['name']) ? $_POST['name'] : null;
135
-		$owner = isset($_POST['owner']) ? $_POST['owner'] : null;
136
-		$sharedBy = isset($_POST['sharedBy']) ? $_POST['sharedBy'] : null;
137
-		$shareWith = isset($_POST['shareWith']) ? $_POST['shareWith'] : null;
138
-		$remoteId = isset($_POST['remoteId']) ? (int)$_POST['remoteId'] : null;
139
-		$sharedByFederatedId = isset($_POST['sharedByFederatedId']) ? $_POST['sharedByFederatedId'] : null;
140
-		$ownerFederatedId = isset($_POST['ownerFederatedId']) ? $_POST['ownerFederatedId'] : null;
141
-
142
-		if ($remote && $token && $name && $owner && $remoteId && $shareWith) {
143
-
144
-			if (!\OCP\Util::isValidFileName($name)) {
145
-				throw new OCSException('The mountpoint name contains invalid characters.', 400);
146
-			}
147
-
148
-			// FIXME this should be a method in the user management instead
149
-			$this->logger->debug('shareWith before, ' . $shareWith, ['app' => 'files_sharing']);
150
-			\OCP\Util::emitHook(
151
-				'\OCA\Files_Sharing\API\Server2Server',
152
-				'preLoginNameUsedAsUserName',
153
-				array('uid' => &$shareWith)
154
-			);
155
-			$this->logger->debug('shareWith after, ' . $shareWith, ['app' => 'files_sharing']);
156
-
157
-			if (!\OC::$server->getUserManager()->userExists($shareWith)) {
158
-				throw new OCSException('User does not exists', 400);
159
-			}
160
-
161
-			\OC_Util::setupFS($shareWith);
162
-
163
-			$externalManager = new \OCA\Files_Sharing\External\Manager(
164
-					\OC::$server->getDatabaseConnection(),
165
-					\OC\Files\Filesystem::getMountManager(),
166
-					\OC\Files\Filesystem::getLoader(),
167
-					\OC::$server->getHTTPClientService(),
168
-					\OC::$server->getNotificationManager(),
169
-					\OC::$server->query(\OCP\OCS\IDiscoveryService::class),
170
-					$shareWith
171
-				);
172
-
173
-			try {
174
-				$externalManager->addShare($remote, $token, '', $name, $owner, false, $shareWith, $remoteId);
175
-				$shareId = \OC::$server->getDatabaseConnection()->lastInsertId('*PREFIX*share_external');
176
-
177
-				if ($ownerFederatedId === null) {
178
-					$ownerFederatedId = $this->cloudIdManager->getCloudId($owner, $this->cleanupRemote($remote))->getId();
179
-				}
180
-				// if the owner of the share and the initiator are the same user
181
-				// we also complete the federated share ID for the initiator
182
-				if ($sharedByFederatedId === null && $owner === $sharedBy) {
183
-					$sharedByFederatedId = $ownerFederatedId;
184
-				}
185
-
186
-				$event = \OC::$server->getActivityManager()->generateEvent();
187
-				$event->setApp('files_sharing')
188
-					->setType('remote_share')
189
-					->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_RECEIVED, [$ownerFederatedId, trim($name, '/')])
190
-					->setAffectedUser($shareWith)
191
-					->setObject('remote_share', (int)$shareId, $name);
192
-				\OC::$server->getActivityManager()->publish($event);
193
-
194
-				$urlGenerator = \OC::$server->getURLGenerator();
195
-
196
-				$notificationManager = \OC::$server->getNotificationManager();
197
-				$notification = $notificationManager->createNotification();
198
-				$notification->setApp('files_sharing')
199
-					->setUser($shareWith)
200
-					->setDateTime(new \DateTime())
201
-					->setObject('remote_share', $shareId)
202
-					->setSubject('remote_share', [$ownerFederatedId, $sharedByFederatedId, trim($name, '/')]);
203
-
204
-				$declineAction = $notification->createAction();
205
-				$declineAction->setLabel('decline')
206
-					->setLink($urlGenerator->getAbsoluteURL($urlGenerator->linkTo('', 'ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/' . $shareId)), 'DELETE');
207
-				$notification->addAction($declineAction);
208
-
209
-				$acceptAction = $notification->createAction();
210
-				$acceptAction->setLabel('accept')
211
-					->setLink($urlGenerator->getAbsoluteURL($urlGenerator->linkTo('', 'ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/' . $shareId)), 'POST');
212
-				$notification->addAction($acceptAction);
213
-
214
-				$notificationManager->notify($notification);
215
-
216
-				return new Http\DataResponse();
217
-			} catch (\Exception $e) {
218
-				$this->logger->logException($e, [
219
-					'message' => 'Server can not add remote share.',
220
-					'level' => ILogger::ERROR,
221
-					'app' => 'files_sharing'
222
-				]);
223
-				throw new OCSException('internal server error, was not able to add share from ' . $remote, 500);
224
-			}
225
-		}
226
-
227
-		throw new OCSException('server can not add remote share, missing parameter', 400);
228
-	}
229
-
230
-	/**
231
-	 * @NoCSRFRequired
232
-	 * @PublicPage
233
-	 *
234
-	 * create re-share on behalf of another user
235
-	 *
236
-	 * @param int $id
237
-	 * @return Http\DataResponse
238
-	 * @throws OCSBadRequestException
239
-	 * @throws OCSForbiddenException
240
-	 * @throws OCSNotFoundException
241
-	 */
242
-	public function reShare($id) {
243
-
244
-		$token = $this->request->getParam('token', null);
245
-		$shareWith = $this->request->getParam('shareWith', null);
246
-		$permission = (int)$this->request->getParam('permission', null);
247
-		$remoteId = (int)$this->request->getParam('remoteId', null);
248
-
249
-		if ($id === null ||
250
-			$token === null ||
251
-			$shareWith === null ||
252
-			$permission === null ||
253
-			$remoteId === null
254
-		) {
255
-			throw new OCSBadRequestException();
256
-		}
257
-
258
-		try {
259
-			$share = $this->federatedShareProvider->getShareById($id);
260
-		} catch (Share\Exceptions\ShareNotFound $e) {
261
-			throw new OCSNotFoundException();
262
-		}
263
-
264
-		// don't allow to share a file back to the owner
265
-		list($user, $remote) = $this->addressHandler->splitUserRemote($shareWith);
266
-		$owner = $share->getShareOwner();
267
-		$currentServer = $this->addressHandler->generateRemoteURL();
268
-		if ($this->addressHandler->compareAddresses($user, $remote, $owner, $currentServer)) {
269
-			throw new OCSForbiddenException();
270
-		}
271
-
272
-		if ($this->verifyShare($share, $token)) {
273
-
274
-			// check if re-sharing is allowed
275
-			if ($share->getPermissions() | ~Constants::PERMISSION_SHARE) {
276
-				$share->setPermissions($share->getPermissions() & $permission);
277
-				// the recipient of the initial share is now the initiator for the re-share
278
-				$share->setSharedBy($share->getSharedWith());
279
-				$share->setSharedWith($shareWith);
280
-				try {
281
-					$result = $this->federatedShareProvider->create($share);
282
-					$this->federatedShareProvider->storeRemoteId((int)$result->getId(), $remoteId);
283
-					return new Http\DataResponse([
284
-						'token' => $result->getToken(),
285
-						'remoteId' => $result->getId()
286
-					]);
287
-				} catch (\Exception $e) {
288
-					throw new OCSBadRequestException();
289
-				}
290
-			} else {
291
-				throw new OCSForbiddenException();
292
-			}
293
-		}
294
-		throw new OCSBadRequestException();
295
-	}
296
-
297
-	/**
298
-	 * @NoCSRFRequired
299
-	 * @PublicPage
300
-	 *
301
-	 * accept server-to-server share
302
-	 *
303
-	 * @param int $id
304
-	 * @return Http\DataResponse
305
-	 * @throws OCSException
306
-	 */
307
-	public function acceptShare($id) {
308
-
309
-		if (!$this->isS2SEnabled()) {
310
-			throw new OCSException('Server does not support federated cloud sharing', 503);
311
-		}
312
-
313
-		$token = isset($_POST['token']) ? $_POST['token'] : null;
314
-
315
-		try {
316
-			$share = $this->federatedShareProvider->getShareById($id);
317
-		} catch (Share\Exceptions\ShareNotFound $e) {
318
-			return new Http\DataResponse();
319
-		}
320
-
321
-		if ($this->verifyShare($share, $token)) {
322
-			$this->executeAcceptShare($share);
323
-			if ($share->getShareOwner() !== $share->getSharedBy()) {
324
-				list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
325
-				$remoteId = $this->federatedShareProvider->getRemoteId($share);
326
-				$this->notifications->sendAcceptShare($remote, $remoteId, $share->getToken());
327
-			}
328
-		}
329
-
330
-		return new Http\DataResponse();
331
-	}
332
-
333
-	protected function executeAcceptShare(Share\IShare $share) {
334
-		$fileId = (int) $share->getNode()->getId();
335
-		list($file, $link) = $this->getFile($this->getCorrectUid($share), $fileId);
336
-
337
-		$event = \OC::$server->getActivityManager()->generateEvent();
338
-		$event->setApp('files_sharing')
339
-			->setType('remote_share')
340
-			->setAffectedUser($this->getCorrectUid($share))
341
-			->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_ACCEPTED, [$share->getSharedWith(), [$fileId => $file]])
342
-			->setObject('files', $fileId, $file)
343
-			->setLink($link);
344
-		\OC::$server->getActivityManager()->publish($event);
345
-	}
346
-
347
-	/**
348
-	 * @NoCSRFRequired
349
-	 * @PublicPage
350
-	 *
351
-	 * decline server-to-server share
352
-	 *
353
-	 * @param int $id
354
-	 * @return Http\DataResponse
355
-	 * @throws OCSException
356
-	 */
357
-	public function declineShare($id) {
358
-
359
-		if (!$this->isS2SEnabled()) {
360
-			throw new OCSException('Server does not support federated cloud sharing', 503);
361
-		}
362
-
363
-		$token = isset($_POST['token']) ? $_POST['token'] : null;
364
-
365
-		try {
366
-			$share = $this->federatedShareProvider->getShareById($id);
367
-		} catch (Share\Exceptions\ShareNotFound $e) {
368
-			return new Http\DataResponse();
369
-		}
370
-
371
-		if ($this->verifyShare($share, $token)) {
372
-			if ($share->getShareOwner() !== $share->getSharedBy()) {
373
-				list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
374
-				$remoteId = $this->federatedShareProvider->getRemoteId($share);
375
-				$this->notifications->sendDeclineShare($remote, $remoteId, $share->getToken());
376
-			}
377
-			$this->executeDeclineShare($share);
378
-		}
379
-
380
-		return new Http\DataResponse();
381
-	}
382
-
383
-	/**
384
-	 * delete declined share and create a activity
385
-	 *
386
-	 * @param Share\IShare $share
387
-	 */
388
-	protected function executeDeclineShare(Share\IShare $share) {
389
-		$this->federatedShareProvider->removeShareFromTable($share);
390
-		$fileId = (int) $share->getNode()->getId();
391
-		list($file, $link) = $this->getFile($this->getCorrectUid($share), $fileId);
392
-
393
-		$event = \OC::$server->getActivityManager()->generateEvent();
394
-		$event->setApp('files_sharing')
395
-			->setType('remote_share')
396
-			->setAffectedUser($this->getCorrectUid($share))
397
-			->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_DECLINED, [$share->getSharedWith(), [$fileId => $file]])
398
-			->setObject('files', $fileId, $file)
399
-			->setLink($link);
400
-		\OC::$server->getActivityManager()->publish($event);
401
-
402
-	}
403
-
404
-	/**
405
-	 * check if we are the initiator or the owner of a re-share and return the correct UID
406
-	 *
407
-	 * @param Share\IShare $share
408
-	 * @return string
409
-	 */
410
-	protected function getCorrectUid(Share\IShare $share) {
411
-		if ($this->userManager->userExists($share->getShareOwner())) {
412
-			return $share->getShareOwner();
413
-		}
414
-
415
-		return $share->getSharedBy();
416
-	}
417
-
418
-	/**
419
-	 * @NoCSRFRequired
420
-	 * @PublicPage
421
-	 *
422
-	 * remove server-to-server share if it was unshared by the owner
423
-	 *
424
-	 * @param int $id
425
-	 * @return Http\DataResponse
426
-	 * @throws OCSException
427
-	 */
428
-	public function unshare($id) {
429
-
430
-		if (!$this->isS2SEnabled()) {
431
-			throw new OCSException('Server does not support federated cloud sharing', 503);
432
-		}
433
-
434
-		$token = isset($_POST['token']) ? $_POST['token'] : null;
435
-
436
-		$qb = $this->connection->getQueryBuilder();
437
-		$qb->select('*')
438
-			->from('share_external')
439
-			->where(
440
-				$qb->expr()->andX(
441
-					$qb->expr()->eq('remote_id', $qb->createNamedParameter($id)),
442
-					$qb->expr()->eq('share_token', $qb->createNamedParameter($token))
443
-				)
444
-			);
445
-
446
-		$result = $qb->execute();
447
-		$share = $result->fetch();
448
-		$result->closeCursor();
449
-
450
-		if ($token && $id && !empty($share)) {
451
-
452
-			$remote = $this->cleanupRemote($share['remote']);
453
-
454
-			$owner = $this->cloudIdManager->getCloudId($share['owner'], $remote);
455
-			$mountpoint = $share['mountpoint'];
456
-			$user = $share['user'];
457
-
458
-			$qb = $this->connection->getQueryBuilder();
459
-			$qb->delete('share_external')
460
-				->where(
461
-					$qb->expr()->andX(
462
-						$qb->expr()->eq('remote_id', $qb->createNamedParameter($id)),
463
-						$qb->expr()->eq('share_token', $qb->createNamedParameter($token))
464
-					)
465
-				);
466
-
467
-			$result = $qb->execute();
468
-			$result->closeCursor();
469
-
470
-			if ($share['accepted']) {
471
-				$path = trim($mountpoint, '/');
472
-			} else {
473
-				$path = trim($share['name'], '/');
474
-			}
475
-
476
-			$notificationManager = \OC::$server->getNotificationManager();
477
-			$notification = $notificationManager->createNotification();
478
-			$notification->setApp('files_sharing')
479
-				->setUser($share['user'])
480
-				->setObject('remote_share', (int)$share['id']);
481
-			$notificationManager->markProcessed($notification);
482
-
483
-			$event = \OC::$server->getActivityManager()->generateEvent();
484
-			$event->setApp('files_sharing')
485
-				->setType('remote_share')
486
-				->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_UNSHARED, [$owner->getId(), $path])
487
-				->setAffectedUser($user)
488
-				->setObject('remote_share', (int)$share['id'], $path);
489
-			\OC::$server->getActivityManager()->publish($event);
490
-		}
491
-
492
-		return new Http\DataResponse();
493
-	}
494
-
495
-	private function cleanupRemote($remote) {
496
-		$remote = substr($remote, strpos($remote, '://') + 3);
497
-
498
-		return rtrim($remote, '/');
499
-	}
500
-
501
-
502
-	/**
503
-	 * @NoCSRFRequired
504
-	 * @PublicPage
505
-	 *
506
-	 * federated share was revoked, either by the owner or the re-sharer
507
-	 *
508
-	 * @param int $id
509
-	 * @return Http\DataResponse
510
-	 * @throws OCSBadRequestException
511
-	 */
512
-	public function revoke($id) {
513
-		$token = $this->request->getParam('token');
514
-
515
-		$share = $this->federatedShareProvider->getShareById($id);
516
-
517
-		if ($this->verifyShare($share, $token)) {
518
-			$this->federatedShareProvider->removeShareFromTable($share);
519
-			return new Http\DataResponse();
520
-		}
521
-
522
-		throw new OCSBadRequestException();
523
-	}
524
-
525
-	/**
526
-	 * get share
527
-	 *
528
-	 * @param int $id
529
-	 * @param string $token
530
-	 * @return array|bool
531
-	 */
532
-	protected function getShare($id, $token) {
533
-		$query = $this->connection->getQueryBuilder();
534
-		$query->select('*')->from($this->shareTable)
535
-			->where($query->expr()->eq('token', $query->createNamedParameter($token)))
536
-			->andWhere($query->expr()->eq('share_type', $query->createNamedParameter(FederatedShareProvider::SHARE_TYPE_REMOTE)))
537
-			->andWhere($query->expr()->eq('id', $query->createNamedParameter($id)));
538
-
539
-		$result = $query->execute()->fetchAll();
540
-
541
-		if (!empty($result) && isset($result[0])) {
542
-			return $result[0];
543
-		}
544
-
545
-		return false;
546
-	}
547
-
548
-	/**
549
-	 * get file
550
-	 *
551
-	 * @param string $user
552
-	 * @param int $fileSource
553
-	 * @return array with internal path of the file and a absolute link to it
554
-	 */
555
-	private function getFile($user, $fileSource) {
556
-		\OC_Util::setupFS($user);
557
-
558
-		try {
559
-			$file = \OC\Files\Filesystem::getPath($fileSource);
560
-		} catch (NotFoundException $e) {
561
-			$file = null;
562
-		}
563
-		$args = \OC\Files\Filesystem::is_dir($file) ? array('dir' => $file) : array('dir' => dirname($file), 'scrollto' => $file);
564
-		$link = \OCP\Util::linkToAbsolute('files', 'index.php', $args);
565
-
566
-		return array($file, $link);
567
-
568
-	}
569
-
570
-	/**
571
-	 * check if server-to-server sharing is enabled
572
-	 *
573
-	 * @param bool $incoming
574
-	 * @return bool
575
-	 */
576
-	private function isS2SEnabled($incoming = false) {
577
-
578
-		$result = \OCP\App::isEnabled('files_sharing');
579
-
580
-		if ($incoming) {
581
-			$result = $result && $this->federatedShareProvider->isIncomingServer2serverShareEnabled();
582
-		} else {
583
-			$result = $result && $this->federatedShareProvider->isOutgoingServer2serverShareEnabled();
584
-		}
585
-
586
-		return $result;
587
-	}
588
-
589
-	/**
590
-	 * check if we got the right share
591
-	 *
592
-	 * @param Share\IShare $share
593
-	 * @param string $token
594
-	 * @return bool
595
-	 */
596
-	protected function verifyShare(Share\IShare $share, $token) {
597
-		if (
598
-			$share->getShareType() === FederatedShareProvider::SHARE_TYPE_REMOTE &&
599
-			$share->getToken() === $token
600
-		) {
601
-			return true;
602
-		}
603
-
604
-		return false;
605
-	}
606
-
607
-	/**
608
-	 * @NoCSRFRequired
609
-	 * @PublicPage
610
-	 *
611
-	 * update share information to keep federated re-shares in sync
612
-	 *
613
-	 * @param int $id
614
-	 * @return Http\DataResponse
615
-	 * @throws OCSBadRequestException
616
-	 */
617
-	public function updatePermissions($id) {
618
-		$token = $this->request->getParam('token', null);
619
-		$permissions = $this->request->getParam('permissions', null);
620
-
621
-		try {
622
-			$share = $this->federatedShareProvider->getShareById($id);
623
-		} catch (Share\Exceptions\ShareNotFound $e) {
624
-			throw new OCSBadRequestException();
625
-		}
626
-
627
-		$validPermission = ctype_digit($permissions);
628
-		$validToken = $this->verifyShare($share, $token);
629
-		if ($validPermission && $validToken) {
630
-			$this->updatePermissionsInDatabase($share, (int)$permissions);
631
-		} else {
632
-			throw new OCSBadRequestException();
633
-		}
634
-
635
-		return new Http\DataResponse();
636
-	}
637
-
638
-	/**
639
-	 * update permissions in database
640
-	 *
641
-	 * @param IShare $share
642
-	 * @param int $permissions
643
-	 */
644
-	protected function updatePermissionsInDatabase(IShare $share, $permissions) {
645
-		$query = $this->connection->getQueryBuilder();
646
-		$query->update('share')
647
-			->where($query->expr()->eq('id', $query->createNamedParameter($share->getId())))
648
-			->set('permissions', $query->createNamedParameter($permissions))
649
-			->execute();
650
-	}
651
-
652
-	/**
653
-	 * @NoCSRFRequired
654
-	 * @PublicPage
655
-	 *
656
-	 * change the owner of a server-to-server share
657
-	 *
658
-	 * @param int $id
659
-	 * @return Http\DataResponse
660
-	 * @throws \InvalidArgumentException
661
-	 * @throws OCSException
662
-	 */
663
-	public function move($id) {
664
-
665
-		if (!$this->isS2SEnabled()) {
666
-			throw new OCSException('Server does not support federated cloud sharing', 503);
667
-		}
668
-
669
-		$token = $this->request->getParam('token');
670
-		$remote = $this->request->getParam('remote');
671
-		$newRemoteId = $this->request->getParam('remote_id', $id);
672
-		$cloudId = $this->cloudIdManager->resolveCloudId($remote);
673
-
674
-		$qb = $this->connection->getQueryBuilder();
675
-		$query = $qb->update('share_external')
676
-			->set('remote', $qb->createNamedParameter($cloudId->getRemote()))
677
-			->set('owner', $qb->createNamedParameter($cloudId->getUser()))
678
-			->set('remote_id', $qb->createNamedParameter($newRemoteId))
679
-			->where($qb->expr()->eq('remote_id', $qb->createNamedParameter($id)))
680
-			->andWhere($qb->expr()->eq('share_token', $qb->createNamedParameter($token)));
681
-		$affected = $query->execute();
682
-
683
-		if ($affected > 0) {
684
-			return new Http\DataResponse(['remote' => $cloudId->getRemote(), 'owner' => $cloudId->getUser()]);
685
-		} else {
686
-			throw new OCSBadRequestException('Share not found or token invalid');
687
-		}
688
-	}
54
+    /** @var FederatedShareProvider */
55
+    private $federatedShareProvider;
56
+
57
+    /** @var IDBConnection */
58
+    private $connection;
59
+
60
+    /** @var Share\IManager */
61
+    private $shareManager;
62
+
63
+    /** @var Notifications */
64
+    private $notifications;
65
+
66
+    /** @var AddressHandler */
67
+    private $addressHandler;
68
+
69
+    /** @var  IUserManager */
70
+    private $userManager;
71
+
72
+    /** @var string */
73
+    private $shareTable = 'share';
74
+
75
+    /** @var ICloudIdManager */
76
+    private $cloudIdManager;
77
+
78
+    /** @var ILogger */
79
+    private $logger;
80
+
81
+    /**
82
+     * Server2Server constructor.
83
+     *
84
+     * @param string $appName
85
+     * @param IRequest $request
86
+     * @param FederatedShareProvider $federatedShareProvider
87
+     * @param IDBConnection $connection
88
+     * @param Share\IManager $shareManager
89
+     * @param Notifications $notifications
90
+     * @param AddressHandler $addressHandler
91
+     * @param IUserManager $userManager
92
+     * @param ICloudIdManager $cloudIdManager
93
+     */
94
+    public function __construct($appName,
95
+                                IRequest $request,
96
+                                FederatedShareProvider $federatedShareProvider,
97
+                                IDBConnection $connection,
98
+                                Share\IManager $shareManager,
99
+                                Notifications $notifications,
100
+                                AddressHandler $addressHandler,
101
+                                IUserManager $userManager,
102
+                                ICloudIdManager $cloudIdManager,
103
+                                ILogger $logger
104
+    ) {
105
+        parent::__construct($appName, $request);
106
+
107
+        $this->federatedShareProvider = $federatedShareProvider;
108
+        $this->connection = $connection;
109
+        $this->shareManager = $shareManager;
110
+        $this->notifications = $notifications;
111
+        $this->addressHandler = $addressHandler;
112
+        $this->userManager = $userManager;
113
+        $this->cloudIdManager = $cloudIdManager;
114
+        $this->logger = $logger;
115
+    }
116
+
117
+    /**
118
+     * @NoCSRFRequired
119
+     * @PublicPage
120
+     *
121
+     * create a new share
122
+     *
123
+     * @return Http\DataResponse
124
+     * @throws OCSException
125
+     */
126
+    public function createShare() {
127
+
128
+        if (!$this->isS2SEnabled(true)) {
129
+            throw new OCSException('Server does not support federated cloud sharing', 503);
130
+        }
131
+
132
+        $remote = isset($_POST['remote']) ? $_POST['remote'] : null;
133
+        $token = isset($_POST['token']) ? $_POST['token'] : null;
134
+        $name = isset($_POST['name']) ? $_POST['name'] : null;
135
+        $owner = isset($_POST['owner']) ? $_POST['owner'] : null;
136
+        $sharedBy = isset($_POST['sharedBy']) ? $_POST['sharedBy'] : null;
137
+        $shareWith = isset($_POST['shareWith']) ? $_POST['shareWith'] : null;
138
+        $remoteId = isset($_POST['remoteId']) ? (int)$_POST['remoteId'] : null;
139
+        $sharedByFederatedId = isset($_POST['sharedByFederatedId']) ? $_POST['sharedByFederatedId'] : null;
140
+        $ownerFederatedId = isset($_POST['ownerFederatedId']) ? $_POST['ownerFederatedId'] : null;
141
+
142
+        if ($remote && $token && $name && $owner && $remoteId && $shareWith) {
143
+
144
+            if (!\OCP\Util::isValidFileName($name)) {
145
+                throw new OCSException('The mountpoint name contains invalid characters.', 400);
146
+            }
147
+
148
+            // FIXME this should be a method in the user management instead
149
+            $this->logger->debug('shareWith before, ' . $shareWith, ['app' => 'files_sharing']);
150
+            \OCP\Util::emitHook(
151
+                '\OCA\Files_Sharing\API\Server2Server',
152
+                'preLoginNameUsedAsUserName',
153
+                array('uid' => &$shareWith)
154
+            );
155
+            $this->logger->debug('shareWith after, ' . $shareWith, ['app' => 'files_sharing']);
156
+
157
+            if (!\OC::$server->getUserManager()->userExists($shareWith)) {
158
+                throw new OCSException('User does not exists', 400);
159
+            }
160
+
161
+            \OC_Util::setupFS($shareWith);
162
+
163
+            $externalManager = new \OCA\Files_Sharing\External\Manager(
164
+                    \OC::$server->getDatabaseConnection(),
165
+                    \OC\Files\Filesystem::getMountManager(),
166
+                    \OC\Files\Filesystem::getLoader(),
167
+                    \OC::$server->getHTTPClientService(),
168
+                    \OC::$server->getNotificationManager(),
169
+                    \OC::$server->query(\OCP\OCS\IDiscoveryService::class),
170
+                    $shareWith
171
+                );
172
+
173
+            try {
174
+                $externalManager->addShare($remote, $token, '', $name, $owner, false, $shareWith, $remoteId);
175
+                $shareId = \OC::$server->getDatabaseConnection()->lastInsertId('*PREFIX*share_external');
176
+
177
+                if ($ownerFederatedId === null) {
178
+                    $ownerFederatedId = $this->cloudIdManager->getCloudId($owner, $this->cleanupRemote($remote))->getId();
179
+                }
180
+                // if the owner of the share and the initiator are the same user
181
+                // we also complete the federated share ID for the initiator
182
+                if ($sharedByFederatedId === null && $owner === $sharedBy) {
183
+                    $sharedByFederatedId = $ownerFederatedId;
184
+                }
185
+
186
+                $event = \OC::$server->getActivityManager()->generateEvent();
187
+                $event->setApp('files_sharing')
188
+                    ->setType('remote_share')
189
+                    ->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_RECEIVED, [$ownerFederatedId, trim($name, '/')])
190
+                    ->setAffectedUser($shareWith)
191
+                    ->setObject('remote_share', (int)$shareId, $name);
192
+                \OC::$server->getActivityManager()->publish($event);
193
+
194
+                $urlGenerator = \OC::$server->getURLGenerator();
195
+
196
+                $notificationManager = \OC::$server->getNotificationManager();
197
+                $notification = $notificationManager->createNotification();
198
+                $notification->setApp('files_sharing')
199
+                    ->setUser($shareWith)
200
+                    ->setDateTime(new \DateTime())
201
+                    ->setObject('remote_share', $shareId)
202
+                    ->setSubject('remote_share', [$ownerFederatedId, $sharedByFederatedId, trim($name, '/')]);
203
+
204
+                $declineAction = $notification->createAction();
205
+                $declineAction->setLabel('decline')
206
+                    ->setLink($urlGenerator->getAbsoluteURL($urlGenerator->linkTo('', 'ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/' . $shareId)), 'DELETE');
207
+                $notification->addAction($declineAction);
208
+
209
+                $acceptAction = $notification->createAction();
210
+                $acceptAction->setLabel('accept')
211
+                    ->setLink($urlGenerator->getAbsoluteURL($urlGenerator->linkTo('', 'ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/' . $shareId)), 'POST');
212
+                $notification->addAction($acceptAction);
213
+
214
+                $notificationManager->notify($notification);
215
+
216
+                return new Http\DataResponse();
217
+            } catch (\Exception $e) {
218
+                $this->logger->logException($e, [
219
+                    'message' => 'Server can not add remote share.',
220
+                    'level' => ILogger::ERROR,
221
+                    'app' => 'files_sharing'
222
+                ]);
223
+                throw new OCSException('internal server error, was not able to add share from ' . $remote, 500);
224
+            }
225
+        }
226
+
227
+        throw new OCSException('server can not add remote share, missing parameter', 400);
228
+    }
229
+
230
+    /**
231
+     * @NoCSRFRequired
232
+     * @PublicPage
233
+     *
234
+     * create re-share on behalf of another user
235
+     *
236
+     * @param int $id
237
+     * @return Http\DataResponse
238
+     * @throws OCSBadRequestException
239
+     * @throws OCSForbiddenException
240
+     * @throws OCSNotFoundException
241
+     */
242
+    public function reShare($id) {
243
+
244
+        $token = $this->request->getParam('token', null);
245
+        $shareWith = $this->request->getParam('shareWith', null);
246
+        $permission = (int)$this->request->getParam('permission', null);
247
+        $remoteId = (int)$this->request->getParam('remoteId', null);
248
+
249
+        if ($id === null ||
250
+            $token === null ||
251
+            $shareWith === null ||
252
+            $permission === null ||
253
+            $remoteId === null
254
+        ) {
255
+            throw new OCSBadRequestException();
256
+        }
257
+
258
+        try {
259
+            $share = $this->federatedShareProvider->getShareById($id);
260
+        } catch (Share\Exceptions\ShareNotFound $e) {
261
+            throw new OCSNotFoundException();
262
+        }
263
+
264
+        // don't allow to share a file back to the owner
265
+        list($user, $remote) = $this->addressHandler->splitUserRemote($shareWith);
266
+        $owner = $share->getShareOwner();
267
+        $currentServer = $this->addressHandler->generateRemoteURL();
268
+        if ($this->addressHandler->compareAddresses($user, $remote, $owner, $currentServer)) {
269
+            throw new OCSForbiddenException();
270
+        }
271
+
272
+        if ($this->verifyShare($share, $token)) {
273
+
274
+            // check if re-sharing is allowed
275
+            if ($share->getPermissions() | ~Constants::PERMISSION_SHARE) {
276
+                $share->setPermissions($share->getPermissions() & $permission);
277
+                // the recipient of the initial share is now the initiator for the re-share
278
+                $share->setSharedBy($share->getSharedWith());
279
+                $share->setSharedWith($shareWith);
280
+                try {
281
+                    $result = $this->federatedShareProvider->create($share);
282
+                    $this->federatedShareProvider->storeRemoteId((int)$result->getId(), $remoteId);
283
+                    return new Http\DataResponse([
284
+                        'token' => $result->getToken(),
285
+                        'remoteId' => $result->getId()
286
+                    ]);
287
+                } catch (\Exception $e) {
288
+                    throw new OCSBadRequestException();
289
+                }
290
+            } else {
291
+                throw new OCSForbiddenException();
292
+            }
293
+        }
294
+        throw new OCSBadRequestException();
295
+    }
296
+
297
+    /**
298
+     * @NoCSRFRequired
299
+     * @PublicPage
300
+     *
301
+     * accept server-to-server share
302
+     *
303
+     * @param int $id
304
+     * @return Http\DataResponse
305
+     * @throws OCSException
306
+     */
307
+    public function acceptShare($id) {
308
+
309
+        if (!$this->isS2SEnabled()) {
310
+            throw new OCSException('Server does not support federated cloud sharing', 503);
311
+        }
312
+
313
+        $token = isset($_POST['token']) ? $_POST['token'] : null;
314
+
315
+        try {
316
+            $share = $this->federatedShareProvider->getShareById($id);
317
+        } catch (Share\Exceptions\ShareNotFound $e) {
318
+            return new Http\DataResponse();
319
+        }
320
+
321
+        if ($this->verifyShare($share, $token)) {
322
+            $this->executeAcceptShare($share);
323
+            if ($share->getShareOwner() !== $share->getSharedBy()) {
324
+                list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
325
+                $remoteId = $this->federatedShareProvider->getRemoteId($share);
326
+                $this->notifications->sendAcceptShare($remote, $remoteId, $share->getToken());
327
+            }
328
+        }
329
+
330
+        return new Http\DataResponse();
331
+    }
332
+
333
+    protected function executeAcceptShare(Share\IShare $share) {
334
+        $fileId = (int) $share->getNode()->getId();
335
+        list($file, $link) = $this->getFile($this->getCorrectUid($share), $fileId);
336
+
337
+        $event = \OC::$server->getActivityManager()->generateEvent();
338
+        $event->setApp('files_sharing')
339
+            ->setType('remote_share')
340
+            ->setAffectedUser($this->getCorrectUid($share))
341
+            ->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_ACCEPTED, [$share->getSharedWith(), [$fileId => $file]])
342
+            ->setObject('files', $fileId, $file)
343
+            ->setLink($link);
344
+        \OC::$server->getActivityManager()->publish($event);
345
+    }
346
+
347
+    /**
348
+     * @NoCSRFRequired
349
+     * @PublicPage
350
+     *
351
+     * decline server-to-server share
352
+     *
353
+     * @param int $id
354
+     * @return Http\DataResponse
355
+     * @throws OCSException
356
+     */
357
+    public function declineShare($id) {
358
+
359
+        if (!$this->isS2SEnabled()) {
360
+            throw new OCSException('Server does not support federated cloud sharing', 503);
361
+        }
362
+
363
+        $token = isset($_POST['token']) ? $_POST['token'] : null;
364
+
365
+        try {
366
+            $share = $this->federatedShareProvider->getShareById($id);
367
+        } catch (Share\Exceptions\ShareNotFound $e) {
368
+            return new Http\DataResponse();
369
+        }
370
+
371
+        if ($this->verifyShare($share, $token)) {
372
+            if ($share->getShareOwner() !== $share->getSharedBy()) {
373
+                list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy());
374
+                $remoteId = $this->federatedShareProvider->getRemoteId($share);
375
+                $this->notifications->sendDeclineShare($remote, $remoteId, $share->getToken());
376
+            }
377
+            $this->executeDeclineShare($share);
378
+        }
379
+
380
+        return new Http\DataResponse();
381
+    }
382
+
383
+    /**
384
+     * delete declined share and create a activity
385
+     *
386
+     * @param Share\IShare $share
387
+     */
388
+    protected function executeDeclineShare(Share\IShare $share) {
389
+        $this->federatedShareProvider->removeShareFromTable($share);
390
+        $fileId = (int) $share->getNode()->getId();
391
+        list($file, $link) = $this->getFile($this->getCorrectUid($share), $fileId);
392
+
393
+        $event = \OC::$server->getActivityManager()->generateEvent();
394
+        $event->setApp('files_sharing')
395
+            ->setType('remote_share')
396
+            ->setAffectedUser($this->getCorrectUid($share))
397
+            ->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_DECLINED, [$share->getSharedWith(), [$fileId => $file]])
398
+            ->setObject('files', $fileId, $file)
399
+            ->setLink($link);
400
+        \OC::$server->getActivityManager()->publish($event);
401
+
402
+    }
403
+
404
+    /**
405
+     * check if we are the initiator or the owner of a re-share and return the correct UID
406
+     *
407
+     * @param Share\IShare $share
408
+     * @return string
409
+     */
410
+    protected function getCorrectUid(Share\IShare $share) {
411
+        if ($this->userManager->userExists($share->getShareOwner())) {
412
+            return $share->getShareOwner();
413
+        }
414
+
415
+        return $share->getSharedBy();
416
+    }
417
+
418
+    /**
419
+     * @NoCSRFRequired
420
+     * @PublicPage
421
+     *
422
+     * remove server-to-server share if it was unshared by the owner
423
+     *
424
+     * @param int $id
425
+     * @return Http\DataResponse
426
+     * @throws OCSException
427
+     */
428
+    public function unshare($id) {
429
+
430
+        if (!$this->isS2SEnabled()) {
431
+            throw new OCSException('Server does not support federated cloud sharing', 503);
432
+        }
433
+
434
+        $token = isset($_POST['token']) ? $_POST['token'] : null;
435
+
436
+        $qb = $this->connection->getQueryBuilder();
437
+        $qb->select('*')
438
+            ->from('share_external')
439
+            ->where(
440
+                $qb->expr()->andX(
441
+                    $qb->expr()->eq('remote_id', $qb->createNamedParameter($id)),
442
+                    $qb->expr()->eq('share_token', $qb->createNamedParameter($token))
443
+                )
444
+            );
445
+
446
+        $result = $qb->execute();
447
+        $share = $result->fetch();
448
+        $result->closeCursor();
449
+
450
+        if ($token && $id && !empty($share)) {
451
+
452
+            $remote = $this->cleanupRemote($share['remote']);
453
+
454
+            $owner = $this->cloudIdManager->getCloudId($share['owner'], $remote);
455
+            $mountpoint = $share['mountpoint'];
456
+            $user = $share['user'];
457
+
458
+            $qb = $this->connection->getQueryBuilder();
459
+            $qb->delete('share_external')
460
+                ->where(
461
+                    $qb->expr()->andX(
462
+                        $qb->expr()->eq('remote_id', $qb->createNamedParameter($id)),
463
+                        $qb->expr()->eq('share_token', $qb->createNamedParameter($token))
464
+                    )
465
+                );
466
+
467
+            $result = $qb->execute();
468
+            $result->closeCursor();
469
+
470
+            if ($share['accepted']) {
471
+                $path = trim($mountpoint, '/');
472
+            } else {
473
+                $path = trim($share['name'], '/');
474
+            }
475
+
476
+            $notificationManager = \OC::$server->getNotificationManager();
477
+            $notification = $notificationManager->createNotification();
478
+            $notification->setApp('files_sharing')
479
+                ->setUser($share['user'])
480
+                ->setObject('remote_share', (int)$share['id']);
481
+            $notificationManager->markProcessed($notification);
482
+
483
+            $event = \OC::$server->getActivityManager()->generateEvent();
484
+            $event->setApp('files_sharing')
485
+                ->setType('remote_share')
486
+                ->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_UNSHARED, [$owner->getId(), $path])
487
+                ->setAffectedUser($user)
488
+                ->setObject('remote_share', (int)$share['id'], $path);
489
+            \OC::$server->getActivityManager()->publish($event);
490
+        }
491
+
492
+        return new Http\DataResponse();
493
+    }
494
+
495
+    private function cleanupRemote($remote) {
496
+        $remote = substr($remote, strpos($remote, '://') + 3);
497
+
498
+        return rtrim($remote, '/');
499
+    }
500
+
501
+
502
+    /**
503
+     * @NoCSRFRequired
504
+     * @PublicPage
505
+     *
506
+     * federated share was revoked, either by the owner or the re-sharer
507
+     *
508
+     * @param int $id
509
+     * @return Http\DataResponse
510
+     * @throws OCSBadRequestException
511
+     */
512
+    public function revoke($id) {
513
+        $token = $this->request->getParam('token');
514
+
515
+        $share = $this->federatedShareProvider->getShareById($id);
516
+
517
+        if ($this->verifyShare($share, $token)) {
518
+            $this->federatedShareProvider->removeShareFromTable($share);
519
+            return new Http\DataResponse();
520
+        }
521
+
522
+        throw new OCSBadRequestException();
523
+    }
524
+
525
+    /**
526
+     * get share
527
+     *
528
+     * @param int $id
529
+     * @param string $token
530
+     * @return array|bool
531
+     */
532
+    protected function getShare($id, $token) {
533
+        $query = $this->connection->getQueryBuilder();
534
+        $query->select('*')->from($this->shareTable)
535
+            ->where($query->expr()->eq('token', $query->createNamedParameter($token)))
536
+            ->andWhere($query->expr()->eq('share_type', $query->createNamedParameter(FederatedShareProvider::SHARE_TYPE_REMOTE)))
537
+            ->andWhere($query->expr()->eq('id', $query->createNamedParameter($id)));
538
+
539
+        $result = $query->execute()->fetchAll();
540
+
541
+        if (!empty($result) && isset($result[0])) {
542
+            return $result[0];
543
+        }
544
+
545
+        return false;
546
+    }
547
+
548
+    /**
549
+     * get file
550
+     *
551
+     * @param string $user
552
+     * @param int $fileSource
553
+     * @return array with internal path of the file and a absolute link to it
554
+     */
555
+    private function getFile($user, $fileSource) {
556
+        \OC_Util::setupFS($user);
557
+
558
+        try {
559
+            $file = \OC\Files\Filesystem::getPath($fileSource);
560
+        } catch (NotFoundException $e) {
561
+            $file = null;
562
+        }
563
+        $args = \OC\Files\Filesystem::is_dir($file) ? array('dir' => $file) : array('dir' => dirname($file), 'scrollto' => $file);
564
+        $link = \OCP\Util::linkToAbsolute('files', 'index.php', $args);
565
+
566
+        return array($file, $link);
567
+
568
+    }
569
+
570
+    /**
571
+     * check if server-to-server sharing is enabled
572
+     *
573
+     * @param bool $incoming
574
+     * @return bool
575
+     */
576
+    private function isS2SEnabled($incoming = false) {
577
+
578
+        $result = \OCP\App::isEnabled('files_sharing');
579
+
580
+        if ($incoming) {
581
+            $result = $result && $this->federatedShareProvider->isIncomingServer2serverShareEnabled();
582
+        } else {
583
+            $result = $result && $this->federatedShareProvider->isOutgoingServer2serverShareEnabled();
584
+        }
585
+
586
+        return $result;
587
+    }
588
+
589
+    /**
590
+     * check if we got the right share
591
+     *
592
+     * @param Share\IShare $share
593
+     * @param string $token
594
+     * @return bool
595
+     */
596
+    protected function verifyShare(Share\IShare $share, $token) {
597
+        if (
598
+            $share->getShareType() === FederatedShareProvider::SHARE_TYPE_REMOTE &&
599
+            $share->getToken() === $token
600
+        ) {
601
+            return true;
602
+        }
603
+
604
+        return false;
605
+    }
606
+
607
+    /**
608
+     * @NoCSRFRequired
609
+     * @PublicPage
610
+     *
611
+     * update share information to keep federated re-shares in sync
612
+     *
613
+     * @param int $id
614
+     * @return Http\DataResponse
615
+     * @throws OCSBadRequestException
616
+     */
617
+    public function updatePermissions($id) {
618
+        $token = $this->request->getParam('token', null);
619
+        $permissions = $this->request->getParam('permissions', null);
620
+
621
+        try {
622
+            $share = $this->federatedShareProvider->getShareById($id);
623
+        } catch (Share\Exceptions\ShareNotFound $e) {
624
+            throw new OCSBadRequestException();
625
+        }
626
+
627
+        $validPermission = ctype_digit($permissions);
628
+        $validToken = $this->verifyShare($share, $token);
629
+        if ($validPermission && $validToken) {
630
+            $this->updatePermissionsInDatabase($share, (int)$permissions);
631
+        } else {
632
+            throw new OCSBadRequestException();
633
+        }
634
+
635
+        return new Http\DataResponse();
636
+    }
637
+
638
+    /**
639
+     * update permissions in database
640
+     *
641
+     * @param IShare $share
642
+     * @param int $permissions
643
+     */
644
+    protected function updatePermissionsInDatabase(IShare $share, $permissions) {
645
+        $query = $this->connection->getQueryBuilder();
646
+        $query->update('share')
647
+            ->where($query->expr()->eq('id', $query->createNamedParameter($share->getId())))
648
+            ->set('permissions', $query->createNamedParameter($permissions))
649
+            ->execute();
650
+    }
651
+
652
+    /**
653
+     * @NoCSRFRequired
654
+     * @PublicPage
655
+     *
656
+     * change the owner of a server-to-server share
657
+     *
658
+     * @param int $id
659
+     * @return Http\DataResponse
660
+     * @throws \InvalidArgumentException
661
+     * @throws OCSException
662
+     */
663
+    public function move($id) {
664
+
665
+        if (!$this->isS2SEnabled()) {
666
+            throw new OCSException('Server does not support federated cloud sharing', 503);
667
+        }
668
+
669
+        $token = $this->request->getParam('token');
670
+        $remote = $this->request->getParam('remote');
671
+        $newRemoteId = $this->request->getParam('remote_id', $id);
672
+        $cloudId = $this->cloudIdManager->resolveCloudId($remote);
673
+
674
+        $qb = $this->connection->getQueryBuilder();
675
+        $query = $qb->update('share_external')
676
+            ->set('remote', $qb->createNamedParameter($cloudId->getRemote()))
677
+            ->set('owner', $qb->createNamedParameter($cloudId->getUser()))
678
+            ->set('remote_id', $qb->createNamedParameter($newRemoteId))
679
+            ->where($qb->expr()->eq('remote_id', $qb->createNamedParameter($id)))
680
+            ->andWhere($qb->expr()->eq('share_token', $qb->createNamedParameter($token)));
681
+        $affected = $query->execute();
682
+
683
+        if ($affected > 0) {
684
+            return new Http\DataResponse(['remote' => $cloudId->getRemote(), 'owner' => $cloudId->getUser()]);
685
+        } else {
686
+            throw new OCSBadRequestException('Share not found or token invalid');
687
+        }
688
+    }
689 689
 }
Please login to merge, or discard this patch.
apps/federatedfilesharing/lib/Controller/MountPublicLinkController.php 1 patch
Indentation   +173 added lines, -173 removed lines patch added patch discarded remove patch
@@ -58,177 +58,177 @@
 block discarded – undo
58 58
  */
59 59
 class MountPublicLinkController extends Controller {
60 60
 
61
-	/** @var FederatedShareProvider */
62
-	private $federatedShareProvider;
63
-
64
-	/** @var AddressHandler */
65
-	private $addressHandler;
66
-
67
-	/** @var IManager  */
68
-	private $shareManager;
69
-
70
-	/** @var  ISession */
71
-	private $session;
72
-
73
-	/** @var IL10N */
74
-	private $l;
75
-
76
-	/** @var IUserSession */
77
-	private $userSession;
78
-
79
-	/** @var IClientService */
80
-	private $clientService;
81
-
82
-	/** @var ICloudIdManager  */
83
-	private $cloudIdManager;
84
-
85
-	/**
86
-	 * MountPublicLinkController constructor.
87
-	 *
88
-	 * @param string $appName
89
-	 * @param IRequest $request
90
-	 * @param FederatedShareProvider $federatedShareProvider
91
-	 * @param IManager $shareManager
92
-	 * @param AddressHandler $addressHandler
93
-	 * @param ISession $session
94
-	 * @param IL10N $l
95
-	 * @param IUserSession $userSession
96
-	 * @param IClientService $clientService
97
-	 * @param ICloudIdManager $cloudIdManager
98
-	 */
99
-	public function __construct($appName,
100
-								IRequest $request,
101
-								FederatedShareProvider $federatedShareProvider,
102
-								IManager $shareManager,
103
-								AddressHandler $addressHandler,
104
-								ISession $session,
105
-								IL10N $l,
106
-								IUserSession $userSession,
107
-								IClientService $clientService,
108
-								ICloudIdManager $cloudIdManager
109
-	) {
110
-		parent::__construct($appName, $request);
111
-
112
-		$this->federatedShareProvider = $federatedShareProvider;
113
-		$this->shareManager = $shareManager;
114
-		$this->addressHandler = $addressHandler;
115
-		$this->session = $session;
116
-		$this->l = $l;
117
-		$this->userSession = $userSession;
118
-		$this->clientService = $clientService;
119
-		$this->cloudIdManager = $cloudIdManager;
120
-	}
121
-
122
-	/**
123
-	 * send federated share to a user of a public link
124
-	 *
125
-	 * @NoCSRFRequired
126
-	 * @PublicPage
127
-	 * @BruteForceProtection(action=publicLink2FederatedShare)
128
-	 *
129
-	 * @param string $shareWith
130
-	 * @param string $token
131
-	 * @param string $password
132
-	 * @return JSONResponse
133
-	 */
134
-	public function createFederatedShare($shareWith, $token, $password = '') {
135
-
136
-		if (!$this->federatedShareProvider->isOutgoingServer2serverShareEnabled()) {
137
-			return new JSONResponse(
138
-				['message' => 'This server doesn\'t support outgoing federated shares'],
139
-				Http::STATUS_BAD_REQUEST
140
-			);
141
-		}
142
-
143
-		try {
144
-			list(, $server) = $this->addressHandler->splitUserRemote($shareWith);
145
-			$share = $this->shareManager->getShareByToken($token);
146
-		} catch (HintException $e) {
147
-			return new JSONResponse(['message' => $e->getHint()], Http::STATUS_BAD_REQUEST);
148
-		}
149
-
150
-		// make sure that user is authenticated in case of a password protected link
151
-		$storedPassword = $share->getPassword();
152
-		$authenticated = $this->session->get('public_link_authenticated') === $share->getId() ||
153
-			$this->shareManager->checkPassword($share, $password);
154
-		if (!empty($storedPassword) && !$authenticated ) {
155
-			$response = new JSONResponse(
156
-				['message' => 'No permission to access the share'],
157
-				Http::STATUS_BAD_REQUEST
158
-			);
159
-			$response->throttle();
160
-			return $response;
161
-		}
162
-
163
-		$share->setSharedWith($shareWith);
164
-
165
-		try {
166
-			$this->federatedShareProvider->create($share);
167
-		} catch (\Exception $e) {
168
-			\OC::$server->getLogger()->logException($e, [
169
-				'level' => ILogger::WARN,
170
-				'app' => 'federatedfilesharing',
171
-			]);
172
-			return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
173
-		}
174
-
175
-		return new JSONResponse(['remoteUrl' => $server]);
176
-	}
177
-
178
-	/**
179
-	 * ask other server to get a federated share
180
-	 *
181
-	 * @NoAdminRequired
182
-	 *
183
-	 * @param string $token
184
-	 * @param string $remote
185
-	 * @param string $password
186
-	 * @param string $owner (only for legacy reasons, can be removed with legacyMountPublicLink())
187
-	 * @param string $ownerDisplayName (only for legacy reasons, can be removed with legacyMountPublicLink())
188
-	 * @param string $name (only for legacy reasons, can be removed with legacyMountPublicLink())
189
-	 * @return JSONResponse
190
-	 */
191
-	public function askForFederatedShare($token, $remote, $password = '', $owner = '', $ownerDisplayName = '', $name = '') {
192
-		// check if server admin allows to mount public links from other servers
193
-		if ($this->federatedShareProvider->isIncomingServer2serverShareEnabled() === false) {
194
-			return new JSONResponse(['message' => $this->l->t('Server to server sharing is not enabled on this server')], Http::STATUS_BAD_REQUEST);
195
-		}
196
-
197
-		$cloudId = $this->cloudIdManager->getCloudId($this->userSession->getUser()->getUID(), $this->addressHandler->generateRemoteURL());
198
-
199
-		$httpClient = $this->clientService->newClient();
200
-
201
-		try {
202
-			$response = $httpClient->post($remote . '/index.php/apps/federatedfilesharing/createFederatedShare',
203
-				[
204
-					'body' =>
205
-						[
206
-							'token' => $token,
207
-							'shareWith' => rtrim($cloudId->getId(), '/'),
208
-							'password' => $password
209
-						],
210
-					'connect_timeout' => 10,
211
-				]
212
-			);
213
-		} catch (\Exception $e) {
214
-			if (empty($password)) {
215
-				$message = $this->l->t("Couldn't establish a federated share.");
216
-			} else {
217
-				$message = $this->l->t("Couldn't establish a federated share, maybe the password was wrong.");
218
-			}
219
-			return new JSONResponse(['message' => $message], Http::STATUS_BAD_REQUEST);
220
-		}
221
-
222
-		$body = $response->getBody();
223
-		$result = json_decode($body, true);
224
-
225
-		if (is_array($result) && isset($result['remoteUrl'])) {
226
-			return new JSONResponse(['message' => $this->l->t('Federated Share request sent, you will receive an invitation. Check your notifications.')]);
227
-		}
228
-
229
-		// if we doesn't get the expected response we assume that we try to add
230
-		// a federated share from a Nextcloud <= 9 server
231
-		$message = $this->l->t("Couldn't establish a federated share, it looks like the server to federate with is too old (Nextcloud <= 9).");
232
-		return new JSONResponse(['message' => $message], Http::STATUS_BAD_REQUEST);
233
-	}
61
+    /** @var FederatedShareProvider */
62
+    private $federatedShareProvider;
63
+
64
+    /** @var AddressHandler */
65
+    private $addressHandler;
66
+
67
+    /** @var IManager  */
68
+    private $shareManager;
69
+
70
+    /** @var  ISession */
71
+    private $session;
72
+
73
+    /** @var IL10N */
74
+    private $l;
75
+
76
+    /** @var IUserSession */
77
+    private $userSession;
78
+
79
+    /** @var IClientService */
80
+    private $clientService;
81
+
82
+    /** @var ICloudIdManager  */
83
+    private $cloudIdManager;
84
+
85
+    /**
86
+     * MountPublicLinkController constructor.
87
+     *
88
+     * @param string $appName
89
+     * @param IRequest $request
90
+     * @param FederatedShareProvider $federatedShareProvider
91
+     * @param IManager $shareManager
92
+     * @param AddressHandler $addressHandler
93
+     * @param ISession $session
94
+     * @param IL10N $l
95
+     * @param IUserSession $userSession
96
+     * @param IClientService $clientService
97
+     * @param ICloudIdManager $cloudIdManager
98
+     */
99
+    public function __construct($appName,
100
+                                IRequest $request,
101
+                                FederatedShareProvider $federatedShareProvider,
102
+                                IManager $shareManager,
103
+                                AddressHandler $addressHandler,
104
+                                ISession $session,
105
+                                IL10N $l,
106
+                                IUserSession $userSession,
107
+                                IClientService $clientService,
108
+                                ICloudIdManager $cloudIdManager
109
+    ) {
110
+        parent::__construct($appName, $request);
111
+
112
+        $this->federatedShareProvider = $federatedShareProvider;
113
+        $this->shareManager = $shareManager;
114
+        $this->addressHandler = $addressHandler;
115
+        $this->session = $session;
116
+        $this->l = $l;
117
+        $this->userSession = $userSession;
118
+        $this->clientService = $clientService;
119
+        $this->cloudIdManager = $cloudIdManager;
120
+    }
121
+
122
+    /**
123
+     * send federated share to a user of a public link
124
+     *
125
+     * @NoCSRFRequired
126
+     * @PublicPage
127
+     * @BruteForceProtection(action=publicLink2FederatedShare)
128
+     *
129
+     * @param string $shareWith
130
+     * @param string $token
131
+     * @param string $password
132
+     * @return JSONResponse
133
+     */
134
+    public function createFederatedShare($shareWith, $token, $password = '') {
135
+
136
+        if (!$this->federatedShareProvider->isOutgoingServer2serverShareEnabled()) {
137
+            return new JSONResponse(
138
+                ['message' => 'This server doesn\'t support outgoing federated shares'],
139
+                Http::STATUS_BAD_REQUEST
140
+            );
141
+        }
142
+
143
+        try {
144
+            list(, $server) = $this->addressHandler->splitUserRemote($shareWith);
145
+            $share = $this->shareManager->getShareByToken($token);
146
+        } catch (HintException $e) {
147
+            return new JSONResponse(['message' => $e->getHint()], Http::STATUS_BAD_REQUEST);
148
+        }
149
+
150
+        // make sure that user is authenticated in case of a password protected link
151
+        $storedPassword = $share->getPassword();
152
+        $authenticated = $this->session->get('public_link_authenticated') === $share->getId() ||
153
+            $this->shareManager->checkPassword($share, $password);
154
+        if (!empty($storedPassword) && !$authenticated ) {
155
+            $response = new JSONResponse(
156
+                ['message' => 'No permission to access the share'],
157
+                Http::STATUS_BAD_REQUEST
158
+            );
159
+            $response->throttle();
160
+            return $response;
161
+        }
162
+
163
+        $share->setSharedWith($shareWith);
164
+
165
+        try {
166
+            $this->federatedShareProvider->create($share);
167
+        } catch (\Exception $e) {
168
+            \OC::$server->getLogger()->logException($e, [
169
+                'level' => ILogger::WARN,
170
+                'app' => 'federatedfilesharing',
171
+            ]);
172
+            return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
173
+        }
174
+
175
+        return new JSONResponse(['remoteUrl' => $server]);
176
+    }
177
+
178
+    /**
179
+     * ask other server to get a federated share
180
+     *
181
+     * @NoAdminRequired
182
+     *
183
+     * @param string $token
184
+     * @param string $remote
185
+     * @param string $password
186
+     * @param string $owner (only for legacy reasons, can be removed with legacyMountPublicLink())
187
+     * @param string $ownerDisplayName (only for legacy reasons, can be removed with legacyMountPublicLink())
188
+     * @param string $name (only for legacy reasons, can be removed with legacyMountPublicLink())
189
+     * @return JSONResponse
190
+     */
191
+    public function askForFederatedShare($token, $remote, $password = '', $owner = '', $ownerDisplayName = '', $name = '') {
192
+        // check if server admin allows to mount public links from other servers
193
+        if ($this->federatedShareProvider->isIncomingServer2serverShareEnabled() === false) {
194
+            return new JSONResponse(['message' => $this->l->t('Server to server sharing is not enabled on this server')], Http::STATUS_BAD_REQUEST);
195
+        }
196
+
197
+        $cloudId = $this->cloudIdManager->getCloudId($this->userSession->getUser()->getUID(), $this->addressHandler->generateRemoteURL());
198
+
199
+        $httpClient = $this->clientService->newClient();
200
+
201
+        try {
202
+            $response = $httpClient->post($remote . '/index.php/apps/federatedfilesharing/createFederatedShare',
203
+                [
204
+                    'body' =>
205
+                        [
206
+                            'token' => $token,
207
+                            'shareWith' => rtrim($cloudId->getId(), '/'),
208
+                            'password' => $password
209
+                        ],
210
+                    'connect_timeout' => 10,
211
+                ]
212
+            );
213
+        } catch (\Exception $e) {
214
+            if (empty($password)) {
215
+                $message = $this->l->t("Couldn't establish a federated share.");
216
+            } else {
217
+                $message = $this->l->t("Couldn't establish a federated share, maybe the password was wrong.");
218
+            }
219
+            return new JSONResponse(['message' => $message], Http::STATUS_BAD_REQUEST);
220
+        }
221
+
222
+        $body = $response->getBody();
223
+        $result = json_decode($body, true);
224
+
225
+        if (is_array($result) && isset($result['remoteUrl'])) {
226
+            return new JSONResponse(['message' => $this->l->t('Federated Share request sent, you will receive an invitation. Check your notifications.')]);
227
+        }
228
+
229
+        // if we doesn't get the expected response we assume that we try to add
230
+        // a federated share from a Nextcloud <= 9 server
231
+        $message = $this->l->t("Couldn't establish a federated share, it looks like the server to federate with is too old (Nextcloud <= 9).");
232
+        return new JSONResponse(['message' => $message], Http::STATUS_BAD_REQUEST);
233
+    }
234 234
 }
Please login to merge, or discard this patch.
apps/sharebymail/lib/ShareByMailProvider.php 1 patch
Indentation   +1018 added lines, -1018 removed lines patch added patch discarded remove patch
@@ -53,1036 +53,1036 @@
 block discarded – undo
53 53
  */
54 54
 class ShareByMailProvider implements IShareProvider {
55 55
 
56
-	/** @var  IDBConnection */
57
-	private $dbConnection;
58
-
59
-	/** @var ILogger */
60
-	private $logger;
61
-
62
-	/** @var ISecureRandom */
63
-	private $secureRandom;
64
-
65
-	/** @var IUserManager */
66
-	private $userManager;
67
-
68
-	/** @var IRootFolder */
69
-	private $rootFolder;
70
-
71
-	/** @var IL10N */
72
-	private $l;
73
-
74
-	/** @var IMailer */
75
-	private $mailer;
76
-
77
-	/** @var IURLGenerator */
78
-	private $urlGenerator;
79
-
80
-	/** @var IManager  */
81
-	private $activityManager;
82
-
83
-	/** @var SettingsManager */
84
-	private $settingsManager;
85
-
86
-	/** @var Defaults */
87
-	private $defaults;
88
-
89
-	/** @var IHasher */
90
-	private $hasher;
91
-
92
-	/** @var  CapabilitiesManager */
93
-	private $capabilitiesManager;
94
-
95
-	/**
96
-	 * Return the identifier of this provider.
97
-	 *
98
-	 * @return string Containing only [a-zA-Z0-9]
99
-	 */
100
-	public function identifier() {
101
-		return 'ocMailShare';
102
-	}
103
-
104
-	/**
105
-	 * DefaultShareProvider constructor.
106
-	 *
107
-	 * @param IDBConnection $connection
108
-	 * @param ISecureRandom $secureRandom
109
-	 * @param IUserManager $userManager
110
-	 * @param IRootFolder $rootFolder
111
-	 * @param IL10N $l
112
-	 * @param ILogger $logger
113
-	 * @param IMailer $mailer
114
-	 * @param IURLGenerator $urlGenerator
115
-	 * @param IManager $activityManager
116
-	 * @param SettingsManager $settingsManager
117
-	 * @param Defaults $defaults
118
-	 * @param IHasher $hasher
119
-	 * @param CapabilitiesManager $capabilitiesManager
120
-	 */
121
-	public function __construct(
122
-		IDBConnection $connection,
123
-		ISecureRandom $secureRandom,
124
-		IUserManager $userManager,
125
-		IRootFolder $rootFolder,
126
-		IL10N $l,
127
-		ILogger $logger,
128
-		IMailer $mailer,
129
-		IURLGenerator $urlGenerator,
130
-		IManager $activityManager,
131
-		SettingsManager $settingsManager,
132
-		Defaults $defaults,
133
-		IHasher $hasher,
134
-		CapabilitiesManager $capabilitiesManager
135
-	) {
136
-		$this->dbConnection = $connection;
137
-		$this->secureRandom = $secureRandom;
138
-		$this->userManager = $userManager;
139
-		$this->rootFolder = $rootFolder;
140
-		$this->l = $l;
141
-		$this->logger = $logger;
142
-		$this->mailer = $mailer;
143
-		$this->urlGenerator = $urlGenerator;
144
-		$this->activityManager = $activityManager;
145
-		$this->settingsManager = $settingsManager;
146
-		$this->defaults = $defaults;
147
-		$this->hasher = $hasher;
148
-		$this->capabilitiesManager = $capabilitiesManager;
149
-	}
150
-
151
-	/**
152
-	 * Share a path
153
-	 *
154
-	 * @param IShare $share
155
-	 * @return IShare The share object
156
-	 * @throws ShareNotFound
157
-	 * @throws \Exception
158
-	 */
159
-	public function create(IShare $share) {
160
-
161
-		$shareWith = $share->getSharedWith();
162
-		/*
56
+    /** @var  IDBConnection */
57
+    private $dbConnection;
58
+
59
+    /** @var ILogger */
60
+    private $logger;
61
+
62
+    /** @var ISecureRandom */
63
+    private $secureRandom;
64
+
65
+    /** @var IUserManager */
66
+    private $userManager;
67
+
68
+    /** @var IRootFolder */
69
+    private $rootFolder;
70
+
71
+    /** @var IL10N */
72
+    private $l;
73
+
74
+    /** @var IMailer */
75
+    private $mailer;
76
+
77
+    /** @var IURLGenerator */
78
+    private $urlGenerator;
79
+
80
+    /** @var IManager  */
81
+    private $activityManager;
82
+
83
+    /** @var SettingsManager */
84
+    private $settingsManager;
85
+
86
+    /** @var Defaults */
87
+    private $defaults;
88
+
89
+    /** @var IHasher */
90
+    private $hasher;
91
+
92
+    /** @var  CapabilitiesManager */
93
+    private $capabilitiesManager;
94
+
95
+    /**
96
+     * Return the identifier of this provider.
97
+     *
98
+     * @return string Containing only [a-zA-Z0-9]
99
+     */
100
+    public function identifier() {
101
+        return 'ocMailShare';
102
+    }
103
+
104
+    /**
105
+     * DefaultShareProvider constructor.
106
+     *
107
+     * @param IDBConnection $connection
108
+     * @param ISecureRandom $secureRandom
109
+     * @param IUserManager $userManager
110
+     * @param IRootFolder $rootFolder
111
+     * @param IL10N $l
112
+     * @param ILogger $logger
113
+     * @param IMailer $mailer
114
+     * @param IURLGenerator $urlGenerator
115
+     * @param IManager $activityManager
116
+     * @param SettingsManager $settingsManager
117
+     * @param Defaults $defaults
118
+     * @param IHasher $hasher
119
+     * @param CapabilitiesManager $capabilitiesManager
120
+     */
121
+    public function __construct(
122
+        IDBConnection $connection,
123
+        ISecureRandom $secureRandom,
124
+        IUserManager $userManager,
125
+        IRootFolder $rootFolder,
126
+        IL10N $l,
127
+        ILogger $logger,
128
+        IMailer $mailer,
129
+        IURLGenerator $urlGenerator,
130
+        IManager $activityManager,
131
+        SettingsManager $settingsManager,
132
+        Defaults $defaults,
133
+        IHasher $hasher,
134
+        CapabilitiesManager $capabilitiesManager
135
+    ) {
136
+        $this->dbConnection = $connection;
137
+        $this->secureRandom = $secureRandom;
138
+        $this->userManager = $userManager;
139
+        $this->rootFolder = $rootFolder;
140
+        $this->l = $l;
141
+        $this->logger = $logger;
142
+        $this->mailer = $mailer;
143
+        $this->urlGenerator = $urlGenerator;
144
+        $this->activityManager = $activityManager;
145
+        $this->settingsManager = $settingsManager;
146
+        $this->defaults = $defaults;
147
+        $this->hasher = $hasher;
148
+        $this->capabilitiesManager = $capabilitiesManager;
149
+    }
150
+
151
+    /**
152
+     * Share a path
153
+     *
154
+     * @param IShare $share
155
+     * @return IShare The share object
156
+     * @throws ShareNotFound
157
+     * @throws \Exception
158
+     */
159
+    public function create(IShare $share) {
160
+
161
+        $shareWith = $share->getSharedWith();
162
+        /*
163 163
 		 * Check if file is not already shared with the remote user
164 164
 		 */
165
-		$alreadyShared = $this->getSharedWith($shareWith, \OCP\Share::SHARE_TYPE_EMAIL, $share->getNode(), 1, 0);
166
-		if (!empty($alreadyShared)) {
167
-			$message = 'Sharing %s failed, this item is already shared with %s';
168
-			$message_t = $this->l->t('Sharing %s failed, this item is already shared with %s', array($share->getNode()->getName(), $shareWith));
169
-			$this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']);
170
-			throw new \Exception($message_t);
171
-		}
172
-
173
-		// if the admin enforces a password for all mail shares we create a
174
-		// random password and send it to the recipient
175
-		$password = '';
176
-		$passwordEnforced = $this->settingsManager->enforcePasswordProtection();
177
-		if ($passwordEnforced) {
178
-			$password = $this->autoGeneratePassword($share);
179
-		}
180
-
181
-		$shareId = $this->createMailShare($share);
182
-		$send = $this->sendPassword($share, $password);
183
-		if ($passwordEnforced && $send === false) {
184
-			$this->sendPasswordToOwner($share, $password);
185
-		}
186
-
187
-		$this->createShareActivity($share);
188
-		$data = $this->getRawShare($shareId);
189
-
190
-		return $this->createShareObject($data);
191
-
192
-	}
193
-
194
-	/**
195
-	 * auto generate password in case of password enforcement on mail shares
196
-	 *
197
-	 * @param IShare $share
198
-	 * @return string
199
-	 * @throws \Exception
200
-	 */
201
-	protected function autoGeneratePassword($share) {
202
-		$initiatorUser = $this->userManager->get($share->getSharedBy());
203
-		$initiatorEMailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null;
204
-		$allowPasswordByMail = $this->settingsManager->sendPasswordByMail();
205
-
206
-		if ($initiatorEMailAddress === null && !$allowPasswordByMail) {
207
-			throw new \Exception(
208
-				$this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.")
209
-			);
210
-		}
211
-
212
-		$passwordPolicy = $this->getPasswordPolicy();
213
-		$passwordCharset = ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS;
214
-		$passwordLength = 8;
215
-		if (!empty($passwordPolicy)) {
216
-			$passwordLength = (int)$passwordPolicy['minLength'] > 0 ? (int)$passwordPolicy['minLength'] : $passwordLength;
217
-			$passwordCharset .= $passwordPolicy['enforceSpecialCharacters'] ? ISecureRandom::CHAR_SYMBOLS : '';
218
-		}
219
-
220
-		$password = $this->secureRandom->generate($passwordLength, $passwordCharset);
221
-
222
-		$share->setPassword($this->hasher->hash($password));
223
-
224
-		return $password;
225
-	}
226
-
227
-	/**
228
-	 * get password policy
229
-	 *
230
-	 * @return array
231
-	 */
232
-	protected function getPasswordPolicy() {
233
-		$capabilities = $this->capabilitiesManager->getCapabilities();
234
-		if (isset($capabilities['password_policy'])) {
235
-			return $capabilities['password_policy'];
236
-		}
237
-
238
-		return [];
239
-	}
240
-
241
-	/**
242
-	 * create activity if a file/folder was shared by mail
243
-	 *
244
-	 * @param IShare $share
245
-	 */
246
-	protected function createShareActivity(IShare $share) {
247
-
248
-		$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
249
-
250
-		$this->publishActivity(
251
-			Activity::SUBJECT_SHARED_EMAIL_SELF,
252
-			[$userFolder->getRelativePath($share->getNode()->getPath()), $share->getSharedWith()],
253
-			$share->getSharedBy(),
254
-			$share->getNode()->getId(),
255
-			$userFolder->getRelativePath($share->getNode()->getPath())
256
-		);
257
-
258
-		if ($share->getShareOwner() !== $share->getSharedBy()) {
259
-			$ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
260
-			$fileId = $share->getNode()->getId();
261
-			$nodes = $ownerFolder->getById($fileId);
262
-			$ownerPath = $nodes[0]->getPath();
263
-			$this->publishActivity(
264
-				Activity::SUBJECT_SHARED_EMAIL_BY,
265
-				[$ownerFolder->getRelativePath($ownerPath), $share->getSharedWith(), $share->getSharedBy()],
266
-				$share->getShareOwner(),
267
-				$fileId,
268
-				$ownerFolder->getRelativePath($ownerPath)
269
-			);
270
-		}
271
-
272
-	}
273
-
274
-	/**
275
-	 * create activity if a file/folder was shared by mail
276
-	 *
277
-	 * @param IShare $share
278
-	 * @param string $sharedWith
279
-	 * @param bool $sendToSelf
280
-	 */
281
-	protected function createPasswordSendActivity(IShare $share, $sharedWith, $sendToSelf) {
282
-
283
-		$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
284
-
285
-		if ($sendToSelf) {
286
-			$this->publishActivity(
287
-				Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND_SELF,
288
-				[$userFolder->getRelativePath($share->getNode()->getPath())],
289
-				$share->getSharedBy(),
290
-				$share->getNode()->getId(),
291
-				$userFolder->getRelativePath($share->getNode()->getPath())
292
-			);
293
-		} else {
294
-			$this->publishActivity(
295
-				Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND,
296
-				[$userFolder->getRelativePath($share->getNode()->getPath()), $sharedWith],
297
-				$share->getSharedBy(),
298
-				$share->getNode()->getId(),
299
-				$userFolder->getRelativePath($share->getNode()->getPath())
300
-			);
301
-		}
302
-	}
303
-
304
-
305
-	/**
306
-	 * publish activity if a file/folder was shared by mail
307
-	 *
308
-	 * @param $subject
309
-	 * @param $parameters
310
-	 * @param $affectedUser
311
-	 * @param $fileId
312
-	 * @param $filePath
313
-	 */
314
-	protected function publishActivity($subject, $parameters, $affectedUser, $fileId, $filePath) {
315
-		$event = $this->activityManager->generateEvent();
316
-		$event->setApp('sharebymail')
317
-			->setType('shared')
318
-			->setSubject($subject, $parameters)
319
-			->setAffectedUser($affectedUser)
320
-			->setObject('files', $fileId, $filePath);
321
-		$this->activityManager->publish($event);
322
-
323
-	}
324
-
325
-	/**
326
-	 * @param IShare $share
327
-	 * @return int
328
-	 * @throws \Exception
329
-	 */
330
-	protected function createMailShare(IShare $share) {
331
-		$share->setToken($this->generateToken());
332
-		$shareId = $this->addShareToDB(
333
-			$share->getNodeId(),
334
-			$share->getNodeType(),
335
-			$share->getSharedWith(),
336
-			$share->getSharedBy(),
337
-			$share->getShareOwner(),
338
-			$share->getPermissions(),
339
-			$share->getToken(),
340
-			$share->getPassword()
341
-		);
342
-
343
-		try {
344
-			$link = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare',
345
-				['token' => $share->getToken()]);
346
-			$this->sendMailNotification(
347
-				$share->getNode()->getName(),
348
-				$link,
349
-				$share->getSharedBy(),
350
-				$share->getSharedWith(),
351
-				$share->getExpirationDate()
352
-			);
353
-		} catch (HintException $hintException) {
354
-			$this->logger->logException($hintException, [
355
-				'message' => 'Failed to send share by mail.',
356
-				'level' => ILogger::ERROR,
357
-				'app' => 'sharebymail',
358
-			]);
359
-			$this->removeShareFromTable($shareId);
360
-			throw $hintException;
361
-		} catch (\Exception $e) {
362
-			$this->logger->logException($e, [
363
-				'message' => 'Failed to send share by mail.',
364
-				'level' => ILogger::ERROR,
365
-				'app' => 'sharebymail',
366
-			]);
367
-			$this->removeShareFromTable($shareId);
368
-			throw new HintException('Failed to send share by mail',
369
-				$this->l->t('Failed to send share by email'));
370
-		}
371
-
372
-		return $shareId;
373
-
374
-	}
375
-
376
-	/**
377
-	 * @param string $filename
378
-	 * @param string $link
379
-	 * @param string $initiator
380
-	 * @param string $shareWith
381
-	 * @param \DateTime|null $expiration
382
-	 * @throws \Exception If mail couldn't be sent
383
-	 */
384
-	protected function sendMailNotification($filename,
385
-											$link,
386
-											$initiator,
387
-											$shareWith,
388
-											\DateTime $expiration = null) {
389
-		$initiatorUser = $this->userManager->get($initiator);
390
-		$initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator;
391
-		$message = $this->mailer->createMessage();
392
-
393
-		$emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientNotification', [
394
-			'filename' => $filename,
395
-			'link' => $link,
396
-			'initiator' => $initiatorDisplayName,
397
-			'expiration' => $expiration,
398
-			'shareWith' => $shareWith,
399
-		]);
400
-
401
-		$emailTemplate->setSubject($this->l->t('%s shared »%s« with you', array($initiatorDisplayName, $filename)));
402
-		$emailTemplate->addHeader();
403
-		$emailTemplate->addHeading($this->l->t('%s shared »%s« with you', [$initiatorDisplayName, $filename]), false);
404
-		$text = $this->l->t('%s shared »%s« with you.', [$initiatorDisplayName, $filename]);
405
-
406
-		$emailTemplate->addBodyText(
407
-			htmlspecialchars($text . ' ' . $this->l->t('Click the button below to open it.')),
408
-			$text
409
-		);
410
-		$emailTemplate->addBodyButton(
411
-			$this->l->t('Open »%s«', [$filename]),
412
-			$link
413
-		);
414
-
415
-		$message->setTo([$shareWith]);
416
-
417
-		// The "From" contains the sharers name
418
-		$instanceName = $this->defaults->getName();
419
-		$senderName = $this->l->t(
420
-			'%s via %s',
421
-			[
422
-				$initiatorDisplayName,
423
-				$instanceName
424
-			]
425
-		);
426
-		$message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
427
-
428
-		// The "Reply-To" is set to the sharer if an mail address is configured
429
-		// also the default footer contains a "Do not reply" which needs to be adjusted.
430
-		$initiatorEmail = $initiatorUser->getEMailAddress();
431
-		if($initiatorEmail !== null) {
432
-			$message->setReplyTo([$initiatorEmail => $initiatorDisplayName]);
433
-			$emailTemplate->addFooter($instanceName . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : ''));
434
-		} else {
435
-			$emailTemplate->addFooter();
436
-		}
437
-
438
-		$message->useTemplate($emailTemplate);
439
-		$this->mailer->send($message);
440
-	}
441
-
442
-	/**
443
-	 * send password to recipient of a mail share
444
-	 *
445
-	 * @param IShare $share
446
-	 * @param string $password
447
-	 * @return bool
448
-	 */
449
-	protected function sendPassword(IShare $share, $password) {
450
-
451
-		$filename = $share->getNode()->getName();
452
-		$initiator = $share->getSharedBy();
453
-		$shareWith = $share->getSharedWith();
454
-
455
-		if ($password === '' || $this->settingsManager->sendPasswordByMail() === false) {
456
-			return false;
457
-		}
458
-
459
-		$initiatorUser = $this->userManager->get($initiator);
460
-		$initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator;
461
-		$initiatorEmailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null;
462
-
463
-		$plainBodyPart = $this->l->t("%s shared »%s« with you.\nYou should have already received a separate mail with a link to access it.\n", [$initiatorDisplayName, $filename]);
464
-		$htmlBodyPart = $this->l->t('%s shared »%s« with you. You should have already received a separate mail with a link to access it.', [$initiatorDisplayName, $filename]);
465
-
466
-		$message = $this->mailer->createMessage();
467
-
468
-		$emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientPasswordNotification', [
469
-			'filename' => $filename,
470
-			'password' => $password,
471
-			'initiator' => $initiatorDisplayName,
472
-			'initiatorEmail' => $initiatorEmailAddress,
473
-			'shareWith' => $shareWith,
474
-		]);
475
-
476
-		$emailTemplate->setSubject($this->l->t('Password to access »%s« shared to you by %s', [$filename, $initiatorDisplayName]));
477
-		$emailTemplate->addHeader();
478
-		$emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false);
479
-		$emailTemplate->addBodyText(htmlspecialchars($htmlBodyPart), $plainBodyPart);
480
-		$emailTemplate->addBodyText($this->l->t('It is protected with the following password: %s', [$password]));
481
-
482
-		// The "From" contains the sharers name
483
-		$instanceName = $this->defaults->getName();
484
-		$senderName = $this->l->t(
485
-			'%s via %s',
486
-			[
487
-				$initiatorDisplayName,
488
-				$instanceName
489
-			]
490
-		);
491
-		$message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
492
-		if ($initiatorEmailAddress !== null) {
493
-			$message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]);
494
-			$emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan());
495
-		} else {
496
-			$emailTemplate->addFooter();
497
-		}
498
-
499
-		$message->setTo([$shareWith]);
500
-		$message->useTemplate($emailTemplate);
501
-		$this->mailer->send($message);
502
-
503
-		$this->createPasswordSendActivity($share, $shareWith, false);
504
-
505
-		return true;
506
-	}
507
-
508
-	/**
509
-	 * send auto generated password to the owner. This happens if the admin enforces
510
-	 * a password for mail shares and forbid to send the password by mail to the recipient
511
-	 *
512
-	 * @param IShare $share
513
-	 * @param string $password
514
-	 * @return bool
515
-	 * @throws \Exception
516
-	 */
517
-	protected function sendPasswordToOwner(IShare $share, $password) {
518
-
519
-		$filename = $share->getNode()->getName();
520
-		$initiator = $this->userManager->get($share->getSharedBy());
521
-		$initiatorEMailAddress = ($initiator instanceof IUser) ? $initiator->getEMailAddress() : null;
522
-		$initiatorDisplayName = ($initiator instanceof IUser) ? $initiator->getDisplayName() : $share->getSharedBy();
523
-		$shareWith = $share->getSharedWith();
524
-
525
-		if ($initiatorEMailAddress === null) {
526
-			throw new \Exception(
527
-				$this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.")
528
-			);
529
-		}
530
-
531
-		$bodyPart = $this->l->t("You just shared »%s« with %s. The share was already send to the recipient. Due to the security policies defined by the administrator of %s each share needs to be protected by password and it is not allowed to send the password directly to the recipient. Therefore you need to forward the password manually to the recipient.", [$filename, $shareWith, $this->defaults->getName()]);
532
-
533
-		$message = $this->mailer->createMessage();
534
-		$emailTemplate = $this->mailer->createEMailTemplate('sharebymail.OwnerPasswordNotification', [
535
-			'filename' => $filename,
536
-			'password' => $password,
537
-			'initiator' => $initiatorDisplayName,
538
-			'initiatorEmail' => $initiatorEMailAddress,
539
-			'shareWith' => $shareWith,
540
-		]);
541
-
542
-		$emailTemplate->setSubject($this->l->t('Password to access »%s« shared with %s', [$filename, $shareWith]));
543
-		$emailTemplate->addHeader();
544
-		$emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false);
545
-		$emailTemplate->addBodyText($bodyPart);
546
-		$emailTemplate->addBodyText($this->l->t('This is the password: %s', [$password]));
547
-		$emailTemplate->addBodyText($this->l->t('You can choose a different password at any time in the share dialog.'));
548
-		$emailTemplate->addFooter();
549
-
550
-		if ($initiatorEMailAddress) {
551
-			$message->setFrom([$initiatorEMailAddress => $initiatorDisplayName]);
552
-		}
553
-		$message->setTo([$initiatorEMailAddress => $initiatorDisplayName]);
554
-		$message->useTemplate($emailTemplate);
555
-		$this->mailer->send($message);
556
-
557
-		$this->createPasswordSendActivity($share, $shareWith, true);
558
-
559
-		return true;
560
-	}
561
-
562
-	/**
563
-	 * generate share token
564
-	 *
565
-	 * @return string
566
-	 */
567
-	protected function generateToken($size = 15) {
568
-		$token = $this->secureRandom->generate($size, ISecureRandom::CHAR_HUMAN_READABLE);
569
-		return $token;
570
-	}
571
-
572
-	/**
573
-	 * Get all children of this share
574
-	 *
575
-	 * @param IShare $parent
576
-	 * @return IShare[]
577
-	 */
578
-	public function getChildren(IShare $parent) {
579
-		$children = [];
580
-
581
-		$qb = $this->dbConnection->getQueryBuilder();
582
-		$qb->select('*')
583
-			->from('share')
584
-			->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId())))
585
-			->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)))
586
-			->orderBy('id');
587
-
588
-		$cursor = $qb->execute();
589
-		while($data = $cursor->fetch()) {
590
-			$children[] = $this->createShareObject($data);
591
-		}
592
-		$cursor->closeCursor();
593
-
594
-		return $children;
595
-	}
596
-
597
-	/**
598
-	 * add share to the database and return the ID
599
-	 *
600
-	 * @param int $itemSource
601
-	 * @param string $itemType
602
-	 * @param string $shareWith
603
-	 * @param string $sharedBy
604
-	 * @param string $uidOwner
605
-	 * @param int $permissions
606
-	 * @param string $token
607
-	 * @return int
608
-	 */
609
-	protected function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $password) {
610
-		$qb = $this->dbConnection->getQueryBuilder();
611
-		$qb->insert('share')
612
-			->setValue('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))
613
-			->setValue('item_type', $qb->createNamedParameter($itemType))
614
-			->setValue('item_source', $qb->createNamedParameter($itemSource))
615
-			->setValue('file_source', $qb->createNamedParameter($itemSource))
616
-			->setValue('share_with', $qb->createNamedParameter($shareWith))
617
-			->setValue('uid_owner', $qb->createNamedParameter($uidOwner))
618
-			->setValue('uid_initiator', $qb->createNamedParameter($sharedBy))
619
-			->setValue('permissions', $qb->createNamedParameter($permissions))
620
-			->setValue('token', $qb->createNamedParameter($token))
621
-			->setValue('password', $qb->createNamedParameter($password))
622
-			->setValue('stime', $qb->createNamedParameter(time()));
623
-
624
-		/*
165
+        $alreadyShared = $this->getSharedWith($shareWith, \OCP\Share::SHARE_TYPE_EMAIL, $share->getNode(), 1, 0);
166
+        if (!empty($alreadyShared)) {
167
+            $message = 'Sharing %s failed, this item is already shared with %s';
168
+            $message_t = $this->l->t('Sharing %s failed, this item is already shared with %s', array($share->getNode()->getName(), $shareWith));
169
+            $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']);
170
+            throw new \Exception($message_t);
171
+        }
172
+
173
+        // if the admin enforces a password for all mail shares we create a
174
+        // random password and send it to the recipient
175
+        $password = '';
176
+        $passwordEnforced = $this->settingsManager->enforcePasswordProtection();
177
+        if ($passwordEnforced) {
178
+            $password = $this->autoGeneratePassword($share);
179
+        }
180
+
181
+        $shareId = $this->createMailShare($share);
182
+        $send = $this->sendPassword($share, $password);
183
+        if ($passwordEnforced && $send === false) {
184
+            $this->sendPasswordToOwner($share, $password);
185
+        }
186
+
187
+        $this->createShareActivity($share);
188
+        $data = $this->getRawShare($shareId);
189
+
190
+        return $this->createShareObject($data);
191
+
192
+    }
193
+
194
+    /**
195
+     * auto generate password in case of password enforcement on mail shares
196
+     *
197
+     * @param IShare $share
198
+     * @return string
199
+     * @throws \Exception
200
+     */
201
+    protected function autoGeneratePassword($share) {
202
+        $initiatorUser = $this->userManager->get($share->getSharedBy());
203
+        $initiatorEMailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null;
204
+        $allowPasswordByMail = $this->settingsManager->sendPasswordByMail();
205
+
206
+        if ($initiatorEMailAddress === null && !$allowPasswordByMail) {
207
+            throw new \Exception(
208
+                $this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.")
209
+            );
210
+        }
211
+
212
+        $passwordPolicy = $this->getPasswordPolicy();
213
+        $passwordCharset = ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS;
214
+        $passwordLength = 8;
215
+        if (!empty($passwordPolicy)) {
216
+            $passwordLength = (int)$passwordPolicy['minLength'] > 0 ? (int)$passwordPolicy['minLength'] : $passwordLength;
217
+            $passwordCharset .= $passwordPolicy['enforceSpecialCharacters'] ? ISecureRandom::CHAR_SYMBOLS : '';
218
+        }
219
+
220
+        $password = $this->secureRandom->generate($passwordLength, $passwordCharset);
221
+
222
+        $share->setPassword($this->hasher->hash($password));
223
+
224
+        return $password;
225
+    }
226
+
227
+    /**
228
+     * get password policy
229
+     *
230
+     * @return array
231
+     */
232
+    protected function getPasswordPolicy() {
233
+        $capabilities = $this->capabilitiesManager->getCapabilities();
234
+        if (isset($capabilities['password_policy'])) {
235
+            return $capabilities['password_policy'];
236
+        }
237
+
238
+        return [];
239
+    }
240
+
241
+    /**
242
+     * create activity if a file/folder was shared by mail
243
+     *
244
+     * @param IShare $share
245
+     */
246
+    protected function createShareActivity(IShare $share) {
247
+
248
+        $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
249
+
250
+        $this->publishActivity(
251
+            Activity::SUBJECT_SHARED_EMAIL_SELF,
252
+            [$userFolder->getRelativePath($share->getNode()->getPath()), $share->getSharedWith()],
253
+            $share->getSharedBy(),
254
+            $share->getNode()->getId(),
255
+            $userFolder->getRelativePath($share->getNode()->getPath())
256
+        );
257
+
258
+        if ($share->getShareOwner() !== $share->getSharedBy()) {
259
+            $ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
260
+            $fileId = $share->getNode()->getId();
261
+            $nodes = $ownerFolder->getById($fileId);
262
+            $ownerPath = $nodes[0]->getPath();
263
+            $this->publishActivity(
264
+                Activity::SUBJECT_SHARED_EMAIL_BY,
265
+                [$ownerFolder->getRelativePath($ownerPath), $share->getSharedWith(), $share->getSharedBy()],
266
+                $share->getShareOwner(),
267
+                $fileId,
268
+                $ownerFolder->getRelativePath($ownerPath)
269
+            );
270
+        }
271
+
272
+    }
273
+
274
+    /**
275
+     * create activity if a file/folder was shared by mail
276
+     *
277
+     * @param IShare $share
278
+     * @param string $sharedWith
279
+     * @param bool $sendToSelf
280
+     */
281
+    protected function createPasswordSendActivity(IShare $share, $sharedWith, $sendToSelf) {
282
+
283
+        $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
284
+
285
+        if ($sendToSelf) {
286
+            $this->publishActivity(
287
+                Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND_SELF,
288
+                [$userFolder->getRelativePath($share->getNode()->getPath())],
289
+                $share->getSharedBy(),
290
+                $share->getNode()->getId(),
291
+                $userFolder->getRelativePath($share->getNode()->getPath())
292
+            );
293
+        } else {
294
+            $this->publishActivity(
295
+                Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND,
296
+                [$userFolder->getRelativePath($share->getNode()->getPath()), $sharedWith],
297
+                $share->getSharedBy(),
298
+                $share->getNode()->getId(),
299
+                $userFolder->getRelativePath($share->getNode()->getPath())
300
+            );
301
+        }
302
+    }
303
+
304
+
305
+    /**
306
+     * publish activity if a file/folder was shared by mail
307
+     *
308
+     * @param $subject
309
+     * @param $parameters
310
+     * @param $affectedUser
311
+     * @param $fileId
312
+     * @param $filePath
313
+     */
314
+    protected function publishActivity($subject, $parameters, $affectedUser, $fileId, $filePath) {
315
+        $event = $this->activityManager->generateEvent();
316
+        $event->setApp('sharebymail')
317
+            ->setType('shared')
318
+            ->setSubject($subject, $parameters)
319
+            ->setAffectedUser($affectedUser)
320
+            ->setObject('files', $fileId, $filePath);
321
+        $this->activityManager->publish($event);
322
+
323
+    }
324
+
325
+    /**
326
+     * @param IShare $share
327
+     * @return int
328
+     * @throws \Exception
329
+     */
330
+    protected function createMailShare(IShare $share) {
331
+        $share->setToken($this->generateToken());
332
+        $shareId = $this->addShareToDB(
333
+            $share->getNodeId(),
334
+            $share->getNodeType(),
335
+            $share->getSharedWith(),
336
+            $share->getSharedBy(),
337
+            $share->getShareOwner(),
338
+            $share->getPermissions(),
339
+            $share->getToken(),
340
+            $share->getPassword()
341
+        );
342
+
343
+        try {
344
+            $link = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare',
345
+                ['token' => $share->getToken()]);
346
+            $this->sendMailNotification(
347
+                $share->getNode()->getName(),
348
+                $link,
349
+                $share->getSharedBy(),
350
+                $share->getSharedWith(),
351
+                $share->getExpirationDate()
352
+            );
353
+        } catch (HintException $hintException) {
354
+            $this->logger->logException($hintException, [
355
+                'message' => 'Failed to send share by mail.',
356
+                'level' => ILogger::ERROR,
357
+                'app' => 'sharebymail',
358
+            ]);
359
+            $this->removeShareFromTable($shareId);
360
+            throw $hintException;
361
+        } catch (\Exception $e) {
362
+            $this->logger->logException($e, [
363
+                'message' => 'Failed to send share by mail.',
364
+                'level' => ILogger::ERROR,
365
+                'app' => 'sharebymail',
366
+            ]);
367
+            $this->removeShareFromTable($shareId);
368
+            throw new HintException('Failed to send share by mail',
369
+                $this->l->t('Failed to send share by email'));
370
+        }
371
+
372
+        return $shareId;
373
+
374
+    }
375
+
376
+    /**
377
+     * @param string $filename
378
+     * @param string $link
379
+     * @param string $initiator
380
+     * @param string $shareWith
381
+     * @param \DateTime|null $expiration
382
+     * @throws \Exception If mail couldn't be sent
383
+     */
384
+    protected function sendMailNotification($filename,
385
+                                            $link,
386
+                                            $initiator,
387
+                                            $shareWith,
388
+                                            \DateTime $expiration = null) {
389
+        $initiatorUser = $this->userManager->get($initiator);
390
+        $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator;
391
+        $message = $this->mailer->createMessage();
392
+
393
+        $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientNotification', [
394
+            'filename' => $filename,
395
+            'link' => $link,
396
+            'initiator' => $initiatorDisplayName,
397
+            'expiration' => $expiration,
398
+            'shareWith' => $shareWith,
399
+        ]);
400
+
401
+        $emailTemplate->setSubject($this->l->t('%s shared »%s« with you', array($initiatorDisplayName, $filename)));
402
+        $emailTemplate->addHeader();
403
+        $emailTemplate->addHeading($this->l->t('%s shared »%s« with you', [$initiatorDisplayName, $filename]), false);
404
+        $text = $this->l->t('%s shared »%s« with you.', [$initiatorDisplayName, $filename]);
405
+
406
+        $emailTemplate->addBodyText(
407
+            htmlspecialchars($text . ' ' . $this->l->t('Click the button below to open it.')),
408
+            $text
409
+        );
410
+        $emailTemplate->addBodyButton(
411
+            $this->l->t('Open »%s«', [$filename]),
412
+            $link
413
+        );
414
+
415
+        $message->setTo([$shareWith]);
416
+
417
+        // The "From" contains the sharers name
418
+        $instanceName = $this->defaults->getName();
419
+        $senderName = $this->l->t(
420
+            '%s via %s',
421
+            [
422
+                $initiatorDisplayName,
423
+                $instanceName
424
+            ]
425
+        );
426
+        $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
427
+
428
+        // The "Reply-To" is set to the sharer if an mail address is configured
429
+        // also the default footer contains a "Do not reply" which needs to be adjusted.
430
+        $initiatorEmail = $initiatorUser->getEMailAddress();
431
+        if($initiatorEmail !== null) {
432
+            $message->setReplyTo([$initiatorEmail => $initiatorDisplayName]);
433
+            $emailTemplate->addFooter($instanceName . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : ''));
434
+        } else {
435
+            $emailTemplate->addFooter();
436
+        }
437
+
438
+        $message->useTemplate($emailTemplate);
439
+        $this->mailer->send($message);
440
+    }
441
+
442
+    /**
443
+     * send password to recipient of a mail share
444
+     *
445
+     * @param IShare $share
446
+     * @param string $password
447
+     * @return bool
448
+     */
449
+    protected function sendPassword(IShare $share, $password) {
450
+
451
+        $filename = $share->getNode()->getName();
452
+        $initiator = $share->getSharedBy();
453
+        $shareWith = $share->getSharedWith();
454
+
455
+        if ($password === '' || $this->settingsManager->sendPasswordByMail() === false) {
456
+            return false;
457
+        }
458
+
459
+        $initiatorUser = $this->userManager->get($initiator);
460
+        $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator;
461
+        $initiatorEmailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null;
462
+
463
+        $plainBodyPart = $this->l->t("%s shared »%s« with you.\nYou should have already received a separate mail with a link to access it.\n", [$initiatorDisplayName, $filename]);
464
+        $htmlBodyPart = $this->l->t('%s shared »%s« with you. You should have already received a separate mail with a link to access it.', [$initiatorDisplayName, $filename]);
465
+
466
+        $message = $this->mailer->createMessage();
467
+
468
+        $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientPasswordNotification', [
469
+            'filename' => $filename,
470
+            'password' => $password,
471
+            'initiator' => $initiatorDisplayName,
472
+            'initiatorEmail' => $initiatorEmailAddress,
473
+            'shareWith' => $shareWith,
474
+        ]);
475
+
476
+        $emailTemplate->setSubject($this->l->t('Password to access »%s« shared to you by %s', [$filename, $initiatorDisplayName]));
477
+        $emailTemplate->addHeader();
478
+        $emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false);
479
+        $emailTemplate->addBodyText(htmlspecialchars($htmlBodyPart), $plainBodyPart);
480
+        $emailTemplate->addBodyText($this->l->t('It is protected with the following password: %s', [$password]));
481
+
482
+        // The "From" contains the sharers name
483
+        $instanceName = $this->defaults->getName();
484
+        $senderName = $this->l->t(
485
+            '%s via %s',
486
+            [
487
+                $initiatorDisplayName,
488
+                $instanceName
489
+            ]
490
+        );
491
+        $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
492
+        if ($initiatorEmailAddress !== null) {
493
+            $message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]);
494
+            $emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan());
495
+        } else {
496
+            $emailTemplate->addFooter();
497
+        }
498
+
499
+        $message->setTo([$shareWith]);
500
+        $message->useTemplate($emailTemplate);
501
+        $this->mailer->send($message);
502
+
503
+        $this->createPasswordSendActivity($share, $shareWith, false);
504
+
505
+        return true;
506
+    }
507
+
508
+    /**
509
+     * send auto generated password to the owner. This happens if the admin enforces
510
+     * a password for mail shares and forbid to send the password by mail to the recipient
511
+     *
512
+     * @param IShare $share
513
+     * @param string $password
514
+     * @return bool
515
+     * @throws \Exception
516
+     */
517
+    protected function sendPasswordToOwner(IShare $share, $password) {
518
+
519
+        $filename = $share->getNode()->getName();
520
+        $initiator = $this->userManager->get($share->getSharedBy());
521
+        $initiatorEMailAddress = ($initiator instanceof IUser) ? $initiator->getEMailAddress() : null;
522
+        $initiatorDisplayName = ($initiator instanceof IUser) ? $initiator->getDisplayName() : $share->getSharedBy();
523
+        $shareWith = $share->getSharedWith();
524
+
525
+        if ($initiatorEMailAddress === null) {
526
+            throw new \Exception(
527
+                $this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.")
528
+            );
529
+        }
530
+
531
+        $bodyPart = $this->l->t("You just shared »%s« with %s. The share was already send to the recipient. Due to the security policies defined by the administrator of %s each share needs to be protected by password and it is not allowed to send the password directly to the recipient. Therefore you need to forward the password manually to the recipient.", [$filename, $shareWith, $this->defaults->getName()]);
532
+
533
+        $message = $this->mailer->createMessage();
534
+        $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.OwnerPasswordNotification', [
535
+            'filename' => $filename,
536
+            'password' => $password,
537
+            'initiator' => $initiatorDisplayName,
538
+            'initiatorEmail' => $initiatorEMailAddress,
539
+            'shareWith' => $shareWith,
540
+        ]);
541
+
542
+        $emailTemplate->setSubject($this->l->t('Password to access »%s« shared with %s', [$filename, $shareWith]));
543
+        $emailTemplate->addHeader();
544
+        $emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false);
545
+        $emailTemplate->addBodyText($bodyPart);
546
+        $emailTemplate->addBodyText($this->l->t('This is the password: %s', [$password]));
547
+        $emailTemplate->addBodyText($this->l->t('You can choose a different password at any time in the share dialog.'));
548
+        $emailTemplate->addFooter();
549
+
550
+        if ($initiatorEMailAddress) {
551
+            $message->setFrom([$initiatorEMailAddress => $initiatorDisplayName]);
552
+        }
553
+        $message->setTo([$initiatorEMailAddress => $initiatorDisplayName]);
554
+        $message->useTemplate($emailTemplate);
555
+        $this->mailer->send($message);
556
+
557
+        $this->createPasswordSendActivity($share, $shareWith, true);
558
+
559
+        return true;
560
+    }
561
+
562
+    /**
563
+     * generate share token
564
+     *
565
+     * @return string
566
+     */
567
+    protected function generateToken($size = 15) {
568
+        $token = $this->secureRandom->generate($size, ISecureRandom::CHAR_HUMAN_READABLE);
569
+        return $token;
570
+    }
571
+
572
+    /**
573
+     * Get all children of this share
574
+     *
575
+     * @param IShare $parent
576
+     * @return IShare[]
577
+     */
578
+    public function getChildren(IShare $parent) {
579
+        $children = [];
580
+
581
+        $qb = $this->dbConnection->getQueryBuilder();
582
+        $qb->select('*')
583
+            ->from('share')
584
+            ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId())))
585
+            ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)))
586
+            ->orderBy('id');
587
+
588
+        $cursor = $qb->execute();
589
+        while($data = $cursor->fetch()) {
590
+            $children[] = $this->createShareObject($data);
591
+        }
592
+        $cursor->closeCursor();
593
+
594
+        return $children;
595
+    }
596
+
597
+    /**
598
+     * add share to the database and return the ID
599
+     *
600
+     * @param int $itemSource
601
+     * @param string $itemType
602
+     * @param string $shareWith
603
+     * @param string $sharedBy
604
+     * @param string $uidOwner
605
+     * @param int $permissions
606
+     * @param string $token
607
+     * @return int
608
+     */
609
+    protected function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $password) {
610
+        $qb = $this->dbConnection->getQueryBuilder();
611
+        $qb->insert('share')
612
+            ->setValue('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))
613
+            ->setValue('item_type', $qb->createNamedParameter($itemType))
614
+            ->setValue('item_source', $qb->createNamedParameter($itemSource))
615
+            ->setValue('file_source', $qb->createNamedParameter($itemSource))
616
+            ->setValue('share_with', $qb->createNamedParameter($shareWith))
617
+            ->setValue('uid_owner', $qb->createNamedParameter($uidOwner))
618
+            ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy))
619
+            ->setValue('permissions', $qb->createNamedParameter($permissions))
620
+            ->setValue('token', $qb->createNamedParameter($token))
621
+            ->setValue('password', $qb->createNamedParameter($password))
622
+            ->setValue('stime', $qb->createNamedParameter(time()));
623
+
624
+        /*
625 625
 		 * Added to fix https://github.com/owncloud/core/issues/22215
626 626
 		 * Can be removed once we get rid of ajax/share.php
627 627
 		 */
628
-		$qb->setValue('file_target', $qb->createNamedParameter(''));
628
+        $qb->setValue('file_target', $qb->createNamedParameter(''));
629 629
 
630
-		$qb->execute();
631
-		$id = $qb->getLastInsertId();
630
+        $qb->execute();
631
+        $id = $qb->getLastInsertId();
632 632
 
633
-		return (int)$id;
634
-	}
633
+        return (int)$id;
634
+    }
635 635
 
636
-	/**
637
-	 * Update a share
638
-	 *
639
-	 * @param IShare $share
640
-	 * @param string|null $plainTextPassword
641
-	 * @return IShare The share object
642
-	 */
643
-	public function update(IShare $share, $plainTextPassword = null) {
636
+    /**
637
+     * Update a share
638
+     *
639
+     * @param IShare $share
640
+     * @param string|null $plainTextPassword
641
+     * @return IShare The share object
642
+     */
643
+    public function update(IShare $share, $plainTextPassword = null) {
644 644
 
645
-		$originalShare = $this->getShareById($share->getId());
645
+        $originalShare = $this->getShareById($share->getId());
646 646
 
647
-		// a real password was given
648
-		$validPassword = $plainTextPassword !== null && $plainTextPassword !== '';
647
+        // a real password was given
648
+        $validPassword = $plainTextPassword !== null && $plainTextPassword !== '';
649 649
 
650
-		if($validPassword && $originalShare->getPassword() !== $share->getPassword()) {
651
-			$this->sendPassword($share, $plainTextPassword);
652
-		}
653
-		/*
650
+        if($validPassword && $originalShare->getPassword() !== $share->getPassword()) {
651
+            $this->sendPassword($share, $plainTextPassword);
652
+        }
653
+        /*
654 654
 		 * We allow updating the permissions and password of mail shares
655 655
 		 */
656
-		$qb = $this->dbConnection->getQueryBuilder();
657
-		$qb->update('share')
658
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId())))
659
-			->set('permissions', $qb->createNamedParameter($share->getPermissions()))
660
-			->set('uid_owner', $qb->createNamedParameter($share->getShareOwner()))
661
-			->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy()))
662
-			->set('password', $qb->createNamedParameter($share->getPassword()))
663
-			->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE))
664
-			->execute();
665
-
666
-		return $share;
667
-	}
668
-
669
-	/**
670
-	 * @inheritdoc
671
-	 */
672
-	public function move(IShare $share, $recipient) {
673
-		/**
674
-		 * nothing to do here, mail shares are only outgoing shares
675
-		 */
676
-		return $share;
677
-	}
678
-
679
-	/**
680
-	 * Delete a share (owner unShares the file)
681
-	 *
682
-	 * @param IShare $share
683
-	 */
684
-	public function delete(IShare $share) {
685
-		$this->removeShareFromTable($share->getId());
686
-	}
687
-
688
-	/**
689
-	 * @inheritdoc
690
-	 */
691
-	public function deleteFromSelf(IShare $share, $recipient) {
692
-		// nothing to do here, mail shares are only outgoing shares
693
-	}
694
-
695
-	/**
696
-	 * @inheritdoc
697
-	 */
698
-	public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) {
699
-		$qb = $this->dbConnection->getQueryBuilder();
700
-		$qb->select('*')
701
-			->from('share');
702
-
703
-		$qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)));
704
-
705
-		/**
706
-		 * Reshares for this user are shares where they are the owner.
707
-		 */
708
-		if ($reshares === false) {
709
-			//Special case for old shares created via the web UI
710
-			$or1 = $qb->expr()->andX(
711
-				$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
712
-				$qb->expr()->isNull('uid_initiator')
713
-			);
714
-
715
-			$qb->andWhere(
716
-				$qb->expr()->orX(
717
-					$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)),
718
-					$or1
719
-				)
720
-			);
721
-		} else {
722
-			$qb->andWhere(
723
-				$qb->expr()->orX(
724
-					$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
725
-					$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
726
-				)
727
-			);
728
-		}
729
-
730
-		if ($node !== null) {
731
-			$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
732
-		}
733
-
734
-		if ($limit !== -1) {
735
-			$qb->setMaxResults($limit);
736
-		}
737
-
738
-		$qb->setFirstResult($offset);
739
-		$qb->orderBy('id');
740
-
741
-		$cursor = $qb->execute();
742
-		$shares = [];
743
-		while($data = $cursor->fetch()) {
744
-			$shares[] = $this->createShareObject($data);
745
-		}
746
-		$cursor->closeCursor();
747
-
748
-		return $shares;
749
-	}
750
-
751
-	/**
752
-	 * @inheritdoc
753
-	 */
754
-	public function getShareById($id, $recipientId = null) {
755
-		$qb = $this->dbConnection->getQueryBuilder();
756
-
757
-		$qb->select('*')
758
-			->from('share')
759
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
760
-			->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)));
761
-
762
-		$cursor = $qb->execute();
763
-		$data = $cursor->fetch();
764
-		$cursor->closeCursor();
765
-
766
-		if ($data === false) {
767
-			throw new ShareNotFound();
768
-		}
769
-
770
-		try {
771
-			$share = $this->createShareObject($data);
772
-		} catch (InvalidShare $e) {
773
-			throw new ShareNotFound();
774
-		}
775
-
776
-		return $share;
777
-	}
778
-
779
-	/**
780
-	 * Get shares for a given path
781
-	 *
782
-	 * @param \OCP\Files\Node $path
783
-	 * @return IShare[]
784
-	 */
785
-	public function getSharesByPath(Node $path) {
786
-		$qb = $this->dbConnection->getQueryBuilder();
787
-
788
-		$cursor = $qb->select('*')
789
-			->from('share')
790
-			->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId())))
791
-			->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)))
792
-			->execute();
793
-
794
-		$shares = [];
795
-		while($data = $cursor->fetch()) {
796
-			$shares[] = $this->createShareObject($data);
797
-		}
798
-		$cursor->closeCursor();
799
-
800
-		return $shares;
801
-	}
802
-
803
-	/**
804
-	 * @inheritdoc
805
-	 */
806
-	public function getSharedWith($userId, $shareType, $node, $limit, $offset) {
807
-		/** @var IShare[] $shares */
808
-		$shares = [];
809
-
810
-		//Get shares directly with this user
811
-		$qb = $this->dbConnection->getQueryBuilder();
812
-		$qb->select('*')
813
-			->from('share');
814
-
815
-		// Order by id
816
-		$qb->orderBy('id');
817
-
818
-		// Set limit and offset
819
-		if ($limit !== -1) {
820
-			$qb->setMaxResults($limit);
821
-		}
822
-		$qb->setFirstResult($offset);
823
-
824
-		$qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)));
825
-		$qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId)));
826
-
827
-		// Filter by node if provided
828
-		if ($node !== null) {
829
-			$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
830
-		}
831
-
832
-		$cursor = $qb->execute();
833
-
834
-		while($data = $cursor->fetch()) {
835
-			$shares[] = $this->createShareObject($data);
836
-		}
837
-		$cursor->closeCursor();
838
-
839
-
840
-		return $shares;
841
-	}
842
-
843
-	/**
844
-	 * Get a share by token
845
-	 *
846
-	 * @param string $token
847
-	 * @return IShare
848
-	 * @throws ShareNotFound
849
-	 */
850
-	public function getShareByToken($token) {
851
-		$qb = $this->dbConnection->getQueryBuilder();
852
-
853
-		$cursor = $qb->select('*')
854
-			->from('share')
855
-			->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)))
856
-			->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)))
857
-			->execute();
858
-
859
-		$data = $cursor->fetch();
860
-
861
-		if ($data === false) {
862
-			throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
863
-		}
864
-
865
-		try {
866
-			$share = $this->createShareObject($data);
867
-		} catch (InvalidShare $e) {
868
-			throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
869
-		}
870
-
871
-		return $share;
872
-	}
873
-
874
-	/**
875
-	 * remove share from table
876
-	 *
877
-	 * @param string $shareId
878
-	 */
879
-	protected function removeShareFromTable($shareId) {
880
-		$qb = $this->dbConnection->getQueryBuilder();
881
-		$qb->delete('share')
882
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId)));
883
-		$qb->execute();
884
-	}
885
-
886
-	/**
887
-	 * Create a share object from an database row
888
-	 *
889
-	 * @param array $data
890
-	 * @return IShare
891
-	 * @throws InvalidShare
892
-	 * @throws ShareNotFound
893
-	 */
894
-	protected function createShareObject($data) {
895
-
896
-		$share = new Share($this->rootFolder, $this->userManager);
897
-		$share->setId((int)$data['id'])
898
-			->setShareType((int)$data['share_type'])
899
-			->setPermissions((int)$data['permissions'])
900
-			->setTarget($data['file_target'])
901
-			->setMailSend((bool)$data['mail_send'])
902
-			->setToken($data['token']);
903
-
904
-		$shareTime = new \DateTime();
905
-		$shareTime->setTimestamp((int)$data['stime']);
906
-		$share->setShareTime($shareTime);
907
-		$share->setSharedWith($data['share_with']);
908
-		$share->setPassword($data['password']);
909
-
910
-		if ($data['uid_initiator'] !== null) {
911
-			$share->setShareOwner($data['uid_owner']);
912
-			$share->setSharedBy($data['uid_initiator']);
913
-		} else {
914
-			//OLD SHARE
915
-			$share->setSharedBy($data['uid_owner']);
916
-			$path = $this->getNode($share->getSharedBy(), (int)$data['file_source']);
917
-
918
-			$owner = $path->getOwner();
919
-			$share->setShareOwner($owner->getUID());
920
-		}
921
-
922
-		if ($data['expiration'] !== null) {
923
-			$expiration = \DateTime::createFromFormat('Y-m-d H:i:s', $data['expiration']);
924
-			if ($expiration !== false) {
925
-				$share->setExpirationDate($expiration);
926
-			}
927
-		}
928
-
929
-		$share->setNodeId((int)$data['file_source']);
930
-		$share->setNodeType($data['item_type']);
931
-
932
-		$share->setProviderId($this->identifier());
933
-
934
-		return $share;
935
-	}
936
-
937
-	/**
938
-	 * Get the node with file $id for $user
939
-	 *
940
-	 * @param string $userId
941
-	 * @param int $id
942
-	 * @return \OCP\Files\File|\OCP\Files\Folder
943
-	 * @throws InvalidShare
944
-	 */
945
-	private function getNode($userId, $id) {
946
-		try {
947
-			$userFolder = $this->rootFolder->getUserFolder($userId);
948
-		} catch (NoUserException $e) {
949
-			throw new InvalidShare();
950
-		}
951
-
952
-		$nodes = $userFolder->getById($id);
953
-
954
-		if (empty($nodes)) {
955
-			throw new InvalidShare();
956
-		}
957
-
958
-		return $nodes[0];
959
-	}
960
-
961
-	/**
962
-	 * A user is deleted from the system
963
-	 * So clean up the relevant shares.
964
-	 *
965
-	 * @param string $uid
966
-	 * @param int $shareType
967
-	 */
968
-	public function userDeleted($uid, $shareType) {
969
-		$qb = $this->dbConnection->getQueryBuilder();
970
-
971
-		$qb->delete('share')
972
-			->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)))
973
-			->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid)))
974
-			->execute();
975
-	}
976
-
977
-	/**
978
-	 * This provider does not support group shares
979
-	 *
980
-	 * @param string $gid
981
-	 */
982
-	public function groupDeleted($gid) {
983
-	}
984
-
985
-	/**
986
-	 * This provider does not support group shares
987
-	 *
988
-	 * @param string $uid
989
-	 * @param string $gid
990
-	 */
991
-	public function userDeletedFromGroup($uid, $gid) {
992
-	}
993
-
994
-	/**
995
-	 * get database row of a give share
996
-	 *
997
-	 * @param $id
998
-	 * @return array
999
-	 * @throws ShareNotFound
1000
-	 */
1001
-	protected function getRawShare($id) {
1002
-
1003
-		// Now fetch the inserted share and create a complete share object
1004
-		$qb = $this->dbConnection->getQueryBuilder();
1005
-		$qb->select('*')
1006
-			->from('share')
1007
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
1008
-
1009
-		$cursor = $qb->execute();
1010
-		$data = $cursor->fetch();
1011
-		$cursor->closeCursor();
1012
-
1013
-		if ($data === false) {
1014
-			throw new ShareNotFound;
1015
-		}
1016
-
1017
-		return $data;
1018
-	}
1019
-
1020
-	public function getSharesInFolder($userId, Folder $node, $reshares) {
1021
-		$qb = $this->dbConnection->getQueryBuilder();
1022
-		$qb->select('*')
1023
-			->from('share', 's')
1024
-			->andWhere($qb->expr()->orX(
1025
-				$qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
1026
-				$qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
1027
-			))
1028
-			->andWhere(
1029
-				$qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))
1030
-			);
1031
-
1032
-		/**
1033
-		 * Reshares for this user are shares where they are the owner.
1034
-		 */
1035
-		if ($reshares === false) {
1036
-			$qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)));
1037
-		} else {
1038
-			$qb->andWhere(
1039
-				$qb->expr()->orX(
1040
-					$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
1041
-					$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
1042
-				)
1043
-			);
1044
-		}
1045
-
1046
-		$qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
1047
-		$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
1048
-
1049
-		$qb->orderBy('id');
1050
-
1051
-		$cursor = $qb->execute();
1052
-		$shares = [];
1053
-		while ($data = $cursor->fetch()) {
1054
-			$shares[$data['fileid']][] = $this->createShareObject($data);
1055
-		}
1056
-		$cursor->closeCursor();
1057
-
1058
-		return $shares;
1059
-	}
1060
-
1061
-	/**
1062
-	 * @inheritdoc
1063
-	 */
1064
-	public function getAccessList($nodes, $currentAccess) {
1065
-		$ids = [];
1066
-		foreach ($nodes as $node) {
1067
-			$ids[] = $node->getId();
1068
-		}
1069
-
1070
-		$qb = $this->dbConnection->getQueryBuilder();
1071
-		$qb->select('share_with')
1072
-			->from('share')
1073
-			->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)))
1074
-			->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)))
1075
-			->andWhere($qb->expr()->orX(
1076
-				$qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
1077
-				$qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
1078
-			))
1079
-			->setMaxResults(1);
1080
-		$cursor = $qb->execute();
1081
-
1082
-		$mail = $cursor->fetch() !== false;
1083
-		$cursor->closeCursor();
1084
-
1085
-		return ['public' => $mail];
1086
-	}
656
+        $qb = $this->dbConnection->getQueryBuilder();
657
+        $qb->update('share')
658
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId())))
659
+            ->set('permissions', $qb->createNamedParameter($share->getPermissions()))
660
+            ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner()))
661
+            ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy()))
662
+            ->set('password', $qb->createNamedParameter($share->getPassword()))
663
+            ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE))
664
+            ->execute();
665
+
666
+        return $share;
667
+    }
668
+
669
+    /**
670
+     * @inheritdoc
671
+     */
672
+    public function move(IShare $share, $recipient) {
673
+        /**
674
+         * nothing to do here, mail shares are only outgoing shares
675
+         */
676
+        return $share;
677
+    }
678
+
679
+    /**
680
+     * Delete a share (owner unShares the file)
681
+     *
682
+     * @param IShare $share
683
+     */
684
+    public function delete(IShare $share) {
685
+        $this->removeShareFromTable($share->getId());
686
+    }
687
+
688
+    /**
689
+     * @inheritdoc
690
+     */
691
+    public function deleteFromSelf(IShare $share, $recipient) {
692
+        // nothing to do here, mail shares are only outgoing shares
693
+    }
694
+
695
+    /**
696
+     * @inheritdoc
697
+     */
698
+    public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) {
699
+        $qb = $this->dbConnection->getQueryBuilder();
700
+        $qb->select('*')
701
+            ->from('share');
702
+
703
+        $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)));
704
+
705
+        /**
706
+         * Reshares for this user are shares where they are the owner.
707
+         */
708
+        if ($reshares === false) {
709
+            //Special case for old shares created via the web UI
710
+            $or1 = $qb->expr()->andX(
711
+                $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
712
+                $qb->expr()->isNull('uid_initiator')
713
+            );
714
+
715
+            $qb->andWhere(
716
+                $qb->expr()->orX(
717
+                    $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)),
718
+                    $or1
719
+                )
720
+            );
721
+        } else {
722
+            $qb->andWhere(
723
+                $qb->expr()->orX(
724
+                    $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
725
+                    $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
726
+                )
727
+            );
728
+        }
729
+
730
+        if ($node !== null) {
731
+            $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
732
+        }
733
+
734
+        if ($limit !== -1) {
735
+            $qb->setMaxResults($limit);
736
+        }
737
+
738
+        $qb->setFirstResult($offset);
739
+        $qb->orderBy('id');
740
+
741
+        $cursor = $qb->execute();
742
+        $shares = [];
743
+        while($data = $cursor->fetch()) {
744
+            $shares[] = $this->createShareObject($data);
745
+        }
746
+        $cursor->closeCursor();
747
+
748
+        return $shares;
749
+    }
750
+
751
+    /**
752
+     * @inheritdoc
753
+     */
754
+    public function getShareById($id, $recipientId = null) {
755
+        $qb = $this->dbConnection->getQueryBuilder();
756
+
757
+        $qb->select('*')
758
+            ->from('share')
759
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
760
+            ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)));
761
+
762
+        $cursor = $qb->execute();
763
+        $data = $cursor->fetch();
764
+        $cursor->closeCursor();
765
+
766
+        if ($data === false) {
767
+            throw new ShareNotFound();
768
+        }
769
+
770
+        try {
771
+            $share = $this->createShareObject($data);
772
+        } catch (InvalidShare $e) {
773
+            throw new ShareNotFound();
774
+        }
775
+
776
+        return $share;
777
+    }
778
+
779
+    /**
780
+     * Get shares for a given path
781
+     *
782
+     * @param \OCP\Files\Node $path
783
+     * @return IShare[]
784
+     */
785
+    public function getSharesByPath(Node $path) {
786
+        $qb = $this->dbConnection->getQueryBuilder();
787
+
788
+        $cursor = $qb->select('*')
789
+            ->from('share')
790
+            ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId())))
791
+            ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)))
792
+            ->execute();
793
+
794
+        $shares = [];
795
+        while($data = $cursor->fetch()) {
796
+            $shares[] = $this->createShareObject($data);
797
+        }
798
+        $cursor->closeCursor();
799
+
800
+        return $shares;
801
+    }
802
+
803
+    /**
804
+     * @inheritdoc
805
+     */
806
+    public function getSharedWith($userId, $shareType, $node, $limit, $offset) {
807
+        /** @var IShare[] $shares */
808
+        $shares = [];
809
+
810
+        //Get shares directly with this user
811
+        $qb = $this->dbConnection->getQueryBuilder();
812
+        $qb->select('*')
813
+            ->from('share');
814
+
815
+        // Order by id
816
+        $qb->orderBy('id');
817
+
818
+        // Set limit and offset
819
+        if ($limit !== -1) {
820
+            $qb->setMaxResults($limit);
821
+        }
822
+        $qb->setFirstResult($offset);
823
+
824
+        $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)));
825
+        $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId)));
826
+
827
+        // Filter by node if provided
828
+        if ($node !== null) {
829
+            $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
830
+        }
831
+
832
+        $cursor = $qb->execute();
833
+
834
+        while($data = $cursor->fetch()) {
835
+            $shares[] = $this->createShareObject($data);
836
+        }
837
+        $cursor->closeCursor();
838
+
839
+
840
+        return $shares;
841
+    }
842
+
843
+    /**
844
+     * Get a share by token
845
+     *
846
+     * @param string $token
847
+     * @return IShare
848
+     * @throws ShareNotFound
849
+     */
850
+    public function getShareByToken($token) {
851
+        $qb = $this->dbConnection->getQueryBuilder();
852
+
853
+        $cursor = $qb->select('*')
854
+            ->from('share')
855
+            ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)))
856
+            ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)))
857
+            ->execute();
858
+
859
+        $data = $cursor->fetch();
860
+
861
+        if ($data === false) {
862
+            throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
863
+        }
864
+
865
+        try {
866
+            $share = $this->createShareObject($data);
867
+        } catch (InvalidShare $e) {
868
+            throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
869
+        }
870
+
871
+        return $share;
872
+    }
873
+
874
+    /**
875
+     * remove share from table
876
+     *
877
+     * @param string $shareId
878
+     */
879
+    protected function removeShareFromTable($shareId) {
880
+        $qb = $this->dbConnection->getQueryBuilder();
881
+        $qb->delete('share')
882
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId)));
883
+        $qb->execute();
884
+    }
885
+
886
+    /**
887
+     * Create a share object from an database row
888
+     *
889
+     * @param array $data
890
+     * @return IShare
891
+     * @throws InvalidShare
892
+     * @throws ShareNotFound
893
+     */
894
+    protected function createShareObject($data) {
895
+
896
+        $share = new Share($this->rootFolder, $this->userManager);
897
+        $share->setId((int)$data['id'])
898
+            ->setShareType((int)$data['share_type'])
899
+            ->setPermissions((int)$data['permissions'])
900
+            ->setTarget($data['file_target'])
901
+            ->setMailSend((bool)$data['mail_send'])
902
+            ->setToken($data['token']);
903
+
904
+        $shareTime = new \DateTime();
905
+        $shareTime->setTimestamp((int)$data['stime']);
906
+        $share->setShareTime($shareTime);
907
+        $share->setSharedWith($data['share_with']);
908
+        $share->setPassword($data['password']);
909
+
910
+        if ($data['uid_initiator'] !== null) {
911
+            $share->setShareOwner($data['uid_owner']);
912
+            $share->setSharedBy($data['uid_initiator']);
913
+        } else {
914
+            //OLD SHARE
915
+            $share->setSharedBy($data['uid_owner']);
916
+            $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']);
917
+
918
+            $owner = $path->getOwner();
919
+            $share->setShareOwner($owner->getUID());
920
+        }
921
+
922
+        if ($data['expiration'] !== null) {
923
+            $expiration = \DateTime::createFromFormat('Y-m-d H:i:s', $data['expiration']);
924
+            if ($expiration !== false) {
925
+                $share->setExpirationDate($expiration);
926
+            }
927
+        }
928
+
929
+        $share->setNodeId((int)$data['file_source']);
930
+        $share->setNodeType($data['item_type']);
931
+
932
+        $share->setProviderId($this->identifier());
933
+
934
+        return $share;
935
+    }
936
+
937
+    /**
938
+     * Get the node with file $id for $user
939
+     *
940
+     * @param string $userId
941
+     * @param int $id
942
+     * @return \OCP\Files\File|\OCP\Files\Folder
943
+     * @throws InvalidShare
944
+     */
945
+    private function getNode($userId, $id) {
946
+        try {
947
+            $userFolder = $this->rootFolder->getUserFolder($userId);
948
+        } catch (NoUserException $e) {
949
+            throw new InvalidShare();
950
+        }
951
+
952
+        $nodes = $userFolder->getById($id);
953
+
954
+        if (empty($nodes)) {
955
+            throw new InvalidShare();
956
+        }
957
+
958
+        return $nodes[0];
959
+    }
960
+
961
+    /**
962
+     * A user is deleted from the system
963
+     * So clean up the relevant shares.
964
+     *
965
+     * @param string $uid
966
+     * @param int $shareType
967
+     */
968
+    public function userDeleted($uid, $shareType) {
969
+        $qb = $this->dbConnection->getQueryBuilder();
970
+
971
+        $qb->delete('share')
972
+            ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)))
973
+            ->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid)))
974
+            ->execute();
975
+    }
976
+
977
+    /**
978
+     * This provider does not support group shares
979
+     *
980
+     * @param string $gid
981
+     */
982
+    public function groupDeleted($gid) {
983
+    }
984
+
985
+    /**
986
+     * This provider does not support group shares
987
+     *
988
+     * @param string $uid
989
+     * @param string $gid
990
+     */
991
+    public function userDeletedFromGroup($uid, $gid) {
992
+    }
993
+
994
+    /**
995
+     * get database row of a give share
996
+     *
997
+     * @param $id
998
+     * @return array
999
+     * @throws ShareNotFound
1000
+     */
1001
+    protected function getRawShare($id) {
1002
+
1003
+        // Now fetch the inserted share and create a complete share object
1004
+        $qb = $this->dbConnection->getQueryBuilder();
1005
+        $qb->select('*')
1006
+            ->from('share')
1007
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
1008
+
1009
+        $cursor = $qb->execute();
1010
+        $data = $cursor->fetch();
1011
+        $cursor->closeCursor();
1012
+
1013
+        if ($data === false) {
1014
+            throw new ShareNotFound;
1015
+        }
1016
+
1017
+        return $data;
1018
+    }
1019
+
1020
+    public function getSharesInFolder($userId, Folder $node, $reshares) {
1021
+        $qb = $this->dbConnection->getQueryBuilder();
1022
+        $qb->select('*')
1023
+            ->from('share', 's')
1024
+            ->andWhere($qb->expr()->orX(
1025
+                $qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
1026
+                $qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
1027
+            ))
1028
+            ->andWhere(
1029
+                $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))
1030
+            );
1031
+
1032
+        /**
1033
+         * Reshares for this user are shares where they are the owner.
1034
+         */
1035
+        if ($reshares === false) {
1036
+            $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)));
1037
+        } else {
1038
+            $qb->andWhere(
1039
+                $qb->expr()->orX(
1040
+                    $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
1041
+                    $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
1042
+                )
1043
+            );
1044
+        }
1045
+
1046
+        $qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
1047
+        $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
1048
+
1049
+        $qb->orderBy('id');
1050
+
1051
+        $cursor = $qb->execute();
1052
+        $shares = [];
1053
+        while ($data = $cursor->fetch()) {
1054
+            $shares[$data['fileid']][] = $this->createShareObject($data);
1055
+        }
1056
+        $cursor->closeCursor();
1057
+
1058
+        return $shares;
1059
+    }
1060
+
1061
+    /**
1062
+     * @inheritdoc
1063
+     */
1064
+    public function getAccessList($nodes, $currentAccess) {
1065
+        $ids = [];
1066
+        foreach ($nodes as $node) {
1067
+            $ids[] = $node->getId();
1068
+        }
1069
+
1070
+        $qb = $this->dbConnection->getQueryBuilder();
1071
+        $qb->select('share_with')
1072
+            ->from('share')
1073
+            ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)))
1074
+            ->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)))
1075
+            ->andWhere($qb->expr()->orX(
1076
+                $qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
1077
+                $qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
1078
+            ))
1079
+            ->setMaxResults(1);
1080
+        $cursor = $qb->execute();
1081
+
1082
+        $mail = $cursor->fetch() !== false;
1083
+        $cursor->closeCursor();
1084
+
1085
+        return ['public' => $mail];
1086
+    }
1087 1087
 
1088 1088
 }
Please login to merge, or discard this patch.
apps/federation/lib/SyncJob.php 1 patch
Indentation   +25 added lines, -25 removed lines patch added patch discarded remove patch
@@ -28,32 +28,32 @@
 block discarded – undo
28 28
 
29 29
 class SyncJob extends TimedJob {
30 30
 
31
-	/** @var SyncFederationAddressBooks */
32
-	protected $syncService;
31
+    /** @var SyncFederationAddressBooks */
32
+    protected $syncService;
33 33
 
34
-	/** @var ILogger */
35
-	protected $logger;
34
+    /** @var ILogger */
35
+    protected $logger;
36 36
 
37
-	/**
38
-	 * @param SyncFederationAddressBooks $syncService
39
-	 * @param ILogger $logger
40
-	 */
41
-	public function __construct(SyncFederationAddressBooks $syncService, ILogger $logger) {
42
-		// Run once a day
43
-		$this->setInterval(24 * 60 * 60);
44
-		$this->syncService = $syncService;
45
-		$this->logger = $logger;
46
-	}
37
+    /**
38
+     * @param SyncFederationAddressBooks $syncService
39
+     * @param ILogger $logger
40
+     */
41
+    public function __construct(SyncFederationAddressBooks $syncService, ILogger $logger) {
42
+        // Run once a day
43
+        $this->setInterval(24 * 60 * 60);
44
+        $this->syncService = $syncService;
45
+        $this->logger = $logger;
46
+    }
47 47
 
48
-	protected function run($argument) {
49
-		$this->syncService->syncThemAll(function($url, $ex) {
50
-			if ($ex instanceof \Exception) {
51
-				$this->logger->logException($ex, [
52
-					'message' => "Error while syncing $url.",
53
-					'level' => ILogger::ERROR,
54
-					'app' => 'fed-sync',
55
-				]);
56
-			}
57
-		});
58
-	}
48
+    protected function run($argument) {
49
+        $this->syncService->syncThemAll(function($url, $ex) {
50
+            if ($ex instanceof \Exception) {
51
+                $this->logger->logException($ex, [
52
+                    'message' => "Error while syncing $url.",
53
+                    'level' => ILogger::ERROR,
54
+                    'app' => 'fed-sync',
55
+                ]);
56
+            }
57
+        });
58
+    }
59 59
 }
Please login to merge, or discard this patch.