Passed
Push — master ( 5079bf...73205a )
by Blizzz
18:18 queued 03:07
created

Wizard::countGroups()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 30
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 20
nc 5
nop 0
dl 0
loc 30
rs 9.2888
c 2
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Alexander Bergolth <[email protected]>
6
 * @author Allan Nordhøy <[email protected]>
7
 * @author Arthur Schiwon <[email protected]>
8
 * @author Bart Visscher <[email protected]>
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Jean-Louis Dupond <[email protected]>
11
 * @author Joas Schilling <[email protected]>
12
 * @author Jörn Friedrich Dreyer <[email protected]>
13
 * @author Lukas Reschke <[email protected]>
14
 * @author Morris Jobke <[email protected]>
15
 * @author Nicolas Grekas <[email protected]>
16
 * @author Robin Appelman <[email protected]>
17
 * @author Robin McCorkell <[email protected]>
18
 * @author Stefan Weil <[email protected]>
19
 * @author Tobias Perschon <[email protected]>
20
 * @author Victor Dubiniuk <[email protected]>
21
 * @author Xuanwo <[email protected]>
22
 *
23
 * @license AGPL-3.0
24
 *
25
 * This code is free software: you can redistribute it and/or modify
26
 * it under the terms of the GNU Affero General Public License, version 3,
27
 * as published by the Free Software Foundation.
28
 *
29
 * This program is distributed in the hope that it will be useful,
30
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
31
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32
 * GNU Affero General Public License for more details.
33
 *
34
 * You should have received a copy of the GNU Affero General Public License, version 3,
35
 * along with this program. If not, see <http://www.gnu.org/licenses/>
36
 *
37
 */
38
namespace OCA\User_LDAP;
39
40
use OC\ServerNotAvailableException;
41
use Psr\Log\LoggerInterface;
42
43
class Wizard extends LDAPUtility {
44
	/** @var \OCP\IL10N */
45
	protected static $l;
46
	protected $access;
47
	protected $cr;
48
	protected $configuration;
49
	protected $result;
50
	protected $resultCache = [];
51
52
	/** @var LoggerInterface */
53
	protected $logger;
54
55
	public const LRESULT_PROCESSED_OK = 2;
56
	public const LRESULT_PROCESSED_INVALID = 3;
57
	public const LRESULT_PROCESSED_SKIP = 4;
58
59
	public const LFILTER_LOGIN = 2;
60
	public const LFILTER_USER_LIST = 3;
61
	public const LFILTER_GROUP_LIST = 4;
62
63
	public const LFILTER_MODE_ASSISTED = 2;
64
	public const LFILTER_MODE_RAW = 1;
65
66
	public const LDAP_NW_TIMEOUT = 4;
67
68
	/**
69
	 * Constructor
70
	 * @param Configuration $configuration an instance of Configuration
71
	 * @param ILDAPWrapper $ldap an instance of ILDAPWrapper
72
	 * @param Access $access
73
	 */
74
	public function __construct(Configuration $configuration, ILDAPWrapper $ldap, Access $access) {
75
		parent::__construct($ldap);
76
		$this->configuration = $configuration;
77
		if (is_null(Wizard::$l)) {
78
			Wizard::$l = \OC::$server->getL10N('user_ldap');
79
		}
80
		$this->access = $access;
81
		$this->result = new WizardResult();
82
		$this->logger = \OC::$server->get(LoggerInterface::class);
83
	}
84
85
	public function __destruct() {
86
		if ($this->result->hasChanges()) {
87
			$this->configuration->saveConfiguration();
88
		}
89
	}
90
91
	/**
92
	 * counts entries in the LDAP directory
93
	 *
94
	 * @param string $filter the LDAP search filter
95
	 * @param string $type a string being either 'users' or 'groups';
96
	 * @return int
97
	 * @throws \Exception
98
	 */
99
	public function countEntries(string $filter, string $type): int {
100
		$reqs = ['ldapHost', 'ldapPort', 'ldapBase'];
101
		if ($type === 'users') {
102
			$reqs[] = 'ldapUserFilter';
103
		}
104
		if (!$this->checkRequirements($reqs)) {
105
			throw new \Exception('Requirements not met', 400);
106
		}
107
108
		$attr = ['dn']; // default
109
		$limit = 1001;
110
		if ($type === 'groups') {
111
			$result = $this->access->countGroups($filter, $attr, $limit);
112
		} elseif ($type === 'users') {
113
			$result = $this->access->countUsers($filter, $attr, $limit);
114
		} elseif ($type === 'objects') {
115
			$result = $this->access->countObjects($limit);
116
		} else {
117
			throw new \Exception('Internal error: Invalid object type', 500);
118
		}
119
120
		return (int)$result;
121
	}
122
123
	public function countGroups() {
124
		$filter = $this->configuration->ldapGroupFilter;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapGroupFilter does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
125
126
		if (empty($filter)) {
127
			$output = self::$l->n('%n group found', '%n groups found', 0);
128
			$this->result->addChange('ldap_group_count', $output);
129
			return $this->result;
130
		}
131
132
		try {
133
			$groupsTotal = $this->countEntries($filter, 'groups');
134
		} catch (\Exception $e) {
135
			//400 can be ignored, 500 is forwarded
136
			if ($e->getCode() === 500) {
137
				throw $e;
138
			}
139
			return false;
140
		}
141
142
		if ($groupsTotal > 1000) {
143
			$output = self::$l->t('> 1000 groups found');
144
		} else {
145
			$output = self::$l->n(
146
				'%n group found',
147
				'%n groups found',
148
				$groupsTotal
149
			);
150
		}
151
		$this->result->addChange('ldap_group_count', $output);
152
		return $this->result;
153
	}
154
155
	/**
156
	 * @return WizardResult
157
	 * @throws \Exception
158
	 */
159
	public function countUsers() {
160
		$filter = $this->access->getFilterForUserCount();
161
162
		$usersTotal = $this->countEntries($filter, 'users');
163
		if ($usersTotal > 1000) {
164
			$output = self::$l->t('> 1000 users found');
165
		} else {
166
			$output = self::$l->n(
167
				'%n user found',
168
				'%n users found',
169
				$usersTotal
170
			);
171
		}
172
		$this->result->addChange('ldap_user_count', $output);
173
		return $this->result;
174
	}
175
176
	/**
177
	 * counts any objects in the currently set base dn
178
	 *
179
	 * @return WizardResult
180
	 * @throws \Exception
181
	 */
182
	public function countInBaseDN() {
183
		// we don't need to provide a filter in this case
184
		$total = $this->countEntries('', 'objects');
185
		if ($total === false) {
0 ignored issues
show
introduced by
The condition $total === false is always false.
Loading history...
186
			throw new \Exception('invalid results received');
187
		}
188
		$this->result->addChange('ldap_test_base', $total);
189
		return $this->result;
190
	}
191
192
	/**
193
	 * counts users with a specified attribute
194
	 * @param string $attr
195
	 * @param bool $existsCheck
196
	 * @return int|bool
197
	 */
198
	public function countUsersWithAttribute($attr, $existsCheck = false) {
199
		if (!$this->checkRequirements(['ldapHost',
200
			'ldapPort',
201
			'ldapBase',
202
			'ldapUserFilter',
203
		])) {
204
			return  false;
205
		}
206
207
		$filter = $this->access->combineFilterWithAnd([
208
			$this->configuration->ldapUserFilter,
0 ignored issues
show
Bug Best Practice introduced by
The property ldapUserFilter does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
209
			$attr . '=*'
210
		]);
211
212
		$limit = ($existsCheck === false) ? null : 1;
213
214
		return $this->access->countUsers($filter, ['dn'], $limit);
215
	}
216
217
	/**
218
	 * detects the display name attribute. If a setting is already present that
219
	 * returns at least one hit, the detection will be canceled.
220
	 * @return WizardResult|bool
221
	 * @throws \Exception
222
	 */
223
	public function detectUserDisplayNameAttribute() {
224
		if (!$this->checkRequirements(['ldapHost',
225
			'ldapPort',
226
			'ldapBase',
227
			'ldapUserFilter',
228
		])) {
229
			return  false;
230
		}
231
232
		$attr = $this->configuration->ldapUserDisplayName;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapUserDisplayName does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
233
		if ($attr !== '' && $attr !== 'displayName') {
234
			// most likely not the default value with upper case N,
235
			// verify it still produces a result
236
			$count = (int)$this->countUsersWithAttribute($attr, true);
237
			if ($count > 0) {
238
				//no change, but we sent it back to make sure the user interface
239
				//is still correct, even if the ajax call was cancelled meanwhile
240
				$this->result->addChange('ldap_display_name', $attr);
241
				return $this->result;
242
			}
243
		}
244
245
		// first attribute that has at least one result wins
246
		$displayNameAttrs = ['displayname', 'cn'];
247
		foreach ($displayNameAttrs as $attr) {
248
			$count = (int)$this->countUsersWithAttribute($attr, true);
249
250
			if ($count > 0) {
251
				$this->applyFind('ldap_display_name', $attr);
252
				return $this->result;
253
			}
254
		}
255
256
		throw new \Exception(self::$l->t('Could not detect user display name attribute. Please specify it yourself in advanced LDAP settings.'));
257
	}
258
259
	/**
260
	 * detects the most often used email attribute for users applying to the
261
	 * user list filter. If a setting is already present that returns at least
262
	 * one hit, the detection will be canceled.
263
	 * @return WizardResult|bool
264
	 */
265
	public function detectEmailAttribute() {
266
		if (!$this->checkRequirements(['ldapHost',
267
			'ldapPort',
268
			'ldapBase',
269
			'ldapUserFilter',
270
		])) {
271
			return  false;
272
		}
273
274
		$attr = $this->configuration->ldapEmailAttribute;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapEmailAttribute does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
275
		if ($attr !== '') {
276
			$count = (int)$this->countUsersWithAttribute($attr, true);
277
			if ($count > 0) {
278
				return false;
279
			}
280
			$writeLog = true;
281
		} else {
282
			$writeLog = false;
283
		}
284
285
		$emailAttributes = ['mail', 'mailPrimaryAddress'];
286
		$winner = '';
287
		$maxUsers = 0;
288
		foreach ($emailAttributes as $attr) {
289
			$count = $this->countUsersWithAttribute($attr);
290
			if ($count > $maxUsers) {
291
				$maxUsers = $count;
292
				$winner = $attr;
293
			}
294
		}
295
296
		if ($winner !== '') {
297
			$this->applyFind('ldap_email_attr', $winner);
298
			if ($writeLog) {
299
				$this->logger->info(
300
					'The mail attribute has automatically been reset, '.
301
					'because the original value did not return any results.',
302
					['app' => 'user_ldap']
303
				);
304
			}
305
		}
306
307
		return $this->result;
308
	}
309
310
	/**
311
	 * @return WizardResult
312
	 * @throws \Exception
313
	 */
314
	public function determineAttributes() {
315
		if (!$this->checkRequirements(['ldapHost',
316
			'ldapPort',
317
			'ldapBase',
318
			'ldapUserFilter',
319
		])) {
320
			return  false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type OCA\User_LDAP\WizardResult.
Loading history...
321
		}
322
323
		$attributes = $this->getUserAttributes();
324
325
		natcasesort($attributes);
326
		$attributes = array_values($attributes);
327
328
		$this->result->addOptions('ldap_loginfilter_attributes', $attributes);
329
330
		$selected = $this->configuration->ldapLoginFilterAttributes;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapLoginFilterAttributes does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
331
		if (is_array($selected) && !empty($selected)) {
332
			$this->result->addChange('ldap_loginfilter_attributes', $selected);
333
		}
334
335
		return $this->result;
336
	}
337
338
	/**
339
	 * detects the available LDAP attributes
340
	 * @return array|false The instance's WizardResult instance
341
	 * @throws \Exception
342
	 */
343
	private function getUserAttributes() {
344
		if (!$this->checkRequirements(['ldapHost',
345
			'ldapPort',
346
			'ldapBase',
347
			'ldapUserFilter',
348
		])) {
349
			return  false;
350
		}
351
		$cr = $this->getConnection();
352
		if (!$cr) {
353
			throw new \Exception('Could not connect to LDAP');
354
		}
355
356
		$base = $this->configuration->ldapBase[0];
0 ignored issues
show
Bug Best Practice introduced by
The property ldapBase does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
357
		$filter = $this->configuration->ldapUserFilter;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapUserFilter does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
358
		$rr = $this->ldap->search($cr, $base, $filter, [], 1, 1);
359
		if (!$this->ldap->isResource($rr)) {
360
			return false;
361
		}
362
		$er = $this->ldap->firstEntry($cr, $rr);
363
		$attributes = $this->ldap->getAttributes($cr, $er);
364
		$pureAttributes = [];
365
		for ($i = 0; $i < $attributes['count']; $i++) {
366
			$pureAttributes[] = $attributes[$i];
367
		}
368
369
		return $pureAttributes;
370
	}
371
372
	/**
373
	 * detects the available LDAP groups
374
	 * @return WizardResult|false the instance's WizardResult instance
375
	 */
376
	public function determineGroupsForGroups() {
377
		return $this->determineGroups('ldap_groupfilter_groups',
378
									  'ldapGroupFilterGroups',
379
									  false);
380
	}
381
382
	/**
383
	 * detects the available LDAP groups
384
	 * @return WizardResult|false the instance's WizardResult instance
385
	 */
386
	public function determineGroupsForUsers() {
387
		return $this->determineGroups('ldap_userfilter_groups',
388
									  'ldapUserFilterGroups');
389
	}
390
391
	/**
392
	 * detects the available LDAP groups
393
	 * @param string $dbKey
394
	 * @param string $confKey
395
	 * @param bool $testMemberOf
396
	 * @return WizardResult|false the instance's WizardResult instance
397
	 * @throws \Exception
398
	 */
399
	private function determineGroups($dbKey, $confKey, $testMemberOf = true) {
400
		if (!$this->checkRequirements(['ldapHost',
401
			'ldapPort',
402
			'ldapBase',
403
		])) {
404
			return  false;
405
		}
406
		$cr = $this->getConnection();
407
		if (!$cr) {
408
			throw new \Exception('Could not connect to LDAP');
409
		}
410
411
		$this->fetchGroups($dbKey, $confKey);
412
413
		if ($testMemberOf) {
414
			$this->configuration->hasMemberOfFilterSupport = $this->testMemberOf();
0 ignored issues
show
Bug Best Practice introduced by
The property hasMemberOfFilterSupport does not exist on OCA\User_LDAP\Configuration. Since you implemented __set, consider adding a @property annotation.
Loading history...
415
			$this->result->markChange();
416
			if (!$this->configuration->hasMemberOfFilterSupport) {
0 ignored issues
show
Bug Best Practice introduced by
The property hasMemberOfFilterSupport does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
417
				throw new \Exception('memberOf is not supported by the server');
418
			}
419
		}
420
421
		return $this->result;
422
	}
423
424
	/**
425
	 * fetches all groups from LDAP and adds them to the result object
426
	 *
427
	 * @param string $dbKey
428
	 * @param string $confKey
429
	 * @return array $groupEntries
430
	 * @throws \Exception
431
	 */
432
	public function fetchGroups($dbKey, $confKey) {
433
		$obclasses = ['posixGroup', 'group', 'zimbraDistributionList', 'groupOfNames', 'groupOfUniqueNames'];
434
435
		$filterParts = [];
436
		foreach ($obclasses as $obclass) {
437
			$filterParts[] = 'objectclass='.$obclass;
438
		}
439
		//we filter for everything
440
		//- that looks like a group and
441
		//- has the group display name set
442
		$filter = $this->access->combineFilterWithOr($filterParts);
443
		$filter = $this->access->combineFilterWithAnd([$filter, 'cn=*']);
444
445
		$groupNames = [];
446
		$groupEntries = [];
447
		$limit = 400;
448
		$offset = 0;
449
		do {
450
			// we need to request dn additionally here, otherwise memberOf
451
			// detection will fail later
452
			$result = $this->access->searchGroups($filter, ['cn', 'dn'], $limit, $offset);
453
			foreach ($result as $item) {
454
				if (!isset($item['cn']) && !is_array($item['cn']) && !isset($item['cn'][0])) {
455
					// just in case - no issue known
456
					continue;
457
				}
458
				$groupNames[] = $item['cn'][0];
459
				$groupEntries[] = $item;
460
			}
461
			$offset += $limit;
462
		} while ($this->access->hasMoreResults());
463
464
		if (count($groupNames) > 0) {
465
			natsort($groupNames);
466
			$this->result->addOptions($dbKey, array_values($groupNames));
467
		} else {
468
			throw new \Exception(self::$l->t('Could not find the desired feature'));
469
		}
470
471
		$setFeatures = $this->configuration->$confKey;
472
		if (is_array($setFeatures) && !empty($setFeatures)) {
473
			//something is already configured? pre-select it.
474
			$this->result->addChange($dbKey, $setFeatures);
475
		}
476
		return $groupEntries;
477
	}
478
479
	public function determineGroupMemberAssoc() {
480
		if (!$this->checkRequirements(['ldapHost',
481
			'ldapPort',
482
			'ldapGroupFilter',
483
		])) {
484
			return  false;
485
		}
486
		$attribute = $this->detectGroupMemberAssoc();
487
		if ($attribute === false) {
488
			return false;
489
		}
490
		$this->configuration->setConfiguration(['ldapGroupMemberAssocAttr' => $attribute]);
491
		$this->result->addChange('ldap_group_member_assoc_attribute', $attribute);
492
493
		return $this->result;
494
	}
495
496
	/**
497
	 * Detects the available object classes
498
	 * @return WizardResult|false the instance's WizardResult instance
499
	 * @throws \Exception
500
	 */
501
	public function determineGroupObjectClasses() {
502
		if (!$this->checkRequirements(['ldapHost',
503
			'ldapPort',
504
			'ldapBase',
505
		])) {
506
			return  false;
507
		}
508
		$cr = $this->getConnection();
509
		if (!$cr) {
510
			throw new \Exception('Could not connect to LDAP');
511
		}
512
513
		$obclasses = ['groupOfNames', 'groupOfUniqueNames', 'group', 'posixGroup', '*'];
514
		$this->determineFeature($obclasses,
515
								'objectclass',
516
								'ldap_groupfilter_objectclass',
517
								'ldapGroupFilterObjectclass',
518
								false);
519
520
		return $this->result;
521
	}
522
523
	/**
524
	 * detects the available object classes
525
	 * @return WizardResult
526
	 * @throws \Exception
527
	 */
528
	public function determineUserObjectClasses() {
529
		if (!$this->checkRequirements(['ldapHost',
530
			'ldapPort',
531
			'ldapBase',
532
		])) {
533
			return  false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type OCA\User_LDAP\WizardResult.
Loading history...
534
		}
535
		$cr = $this->getConnection();
536
		if (!$cr) {
537
			throw new \Exception('Could not connect to LDAP');
538
		}
539
540
		$obclasses = ['inetOrgPerson', 'person', 'organizationalPerson',
541
			'user', 'posixAccount', '*'];
542
		$filter = $this->configuration->ldapUserFilter;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapUserFilter does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
543
		//if filter is empty, it is probably the first time the wizard is called
544
		//then, apply suggestions.
545
		$this->determineFeature($obclasses,
546
								'objectclass',
547
								'ldap_userfilter_objectclass',
548
								'ldapUserFilterObjectclass',
549
								empty($filter));
550
551
		return $this->result;
552
	}
553
554
	/**
555
	 * @return WizardResult|false
556
	 * @throws \Exception
557
	 */
558
	public function getGroupFilter() {
559
		if (!$this->checkRequirements(['ldapHost',
560
			'ldapPort',
561
			'ldapBase',
562
		])) {
563
			return false;
564
		}
565
		//make sure the use display name is set
566
		$displayName = $this->configuration->ldapGroupDisplayName;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapGroupDisplayName does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
567
		if ($displayName === '') {
568
			$d = $this->configuration->getDefaults();
569
			$this->applyFind('ldap_group_display_name',
570
							 $d['ldap_group_display_name']);
571
		}
572
		$filter = $this->composeLdapFilter(self::LFILTER_GROUP_LIST);
573
574
		$this->applyFind('ldap_group_filter', $filter);
575
		return $this->result;
576
	}
577
578
	/**
579
	 * @return WizardResult|false
580
	 * @throws \Exception
581
	 */
582
	public function getUserListFilter() {
583
		if (!$this->checkRequirements(['ldapHost',
584
			'ldapPort',
585
			'ldapBase',
586
		])) {
587
			return false;
588
		}
589
		//make sure the use display name is set
590
		$displayName = $this->configuration->ldapUserDisplayName;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapUserDisplayName does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
591
		if ($displayName === '') {
592
			$d = $this->configuration->getDefaults();
593
			$this->applyFind('ldap_display_name', $d['ldap_display_name']);
594
		}
595
		$filter = $this->composeLdapFilter(self::LFILTER_USER_LIST);
596
		if (!$filter) {
597
			throw new \Exception('Cannot create filter');
598
		}
599
600
		$this->applyFind('ldap_userlist_filter', $filter);
601
		return $this->result;
602
	}
603
604
	/**
605
	 * @return bool|WizardResult
606
	 * @throws \Exception
607
	 */
608
	public function getUserLoginFilter() {
609
		if (!$this->checkRequirements(['ldapHost',
610
			'ldapPort',
611
			'ldapBase',
612
			'ldapUserFilter',
613
		])) {
614
			return false;
615
		}
616
617
		$filter = $this->composeLdapFilter(self::LFILTER_LOGIN);
618
		if (!$filter) {
619
			throw new \Exception('Cannot create filter');
620
		}
621
622
		$this->applyFind('ldap_login_filter', $filter);
623
		return $this->result;
624
	}
625
626
	/**
627
	 * @return bool|WizardResult
628
	 * @param string $loginName
629
	 * @throws \Exception
630
	 */
631
	public function testLoginName($loginName) {
632
		if (!$this->checkRequirements(['ldapHost',
633
			'ldapPort',
634
			'ldapBase',
635
			'ldapLoginFilter',
636
		])) {
637
			return false;
638
		}
639
640
		$cr = $this->access->connection->getConnectionResource();
641
		if (!$this->ldap->isResource($cr)) {
642
			throw new \Exception('connection error');
643
		}
644
645
		if (mb_strpos($this->access->connection->ldapLoginFilter, '%uid', 0, 'UTF-8')
646
			=== false) {
647
			throw new \Exception('missing placeholder');
648
		}
649
650
		$users = $this->access->countUsersByLoginName($loginName);
651
		if ($this->ldap->errno($cr) !== 0) {
652
			throw new \Exception($this->ldap->error($cr));
653
		}
654
		$filter = str_replace('%uid', $loginName, $this->access->connection->ldapLoginFilter);
655
		$this->result->addChange('ldap_test_loginname', $users);
656
		$this->result->addChange('ldap_test_effective_filter', $filter);
657
		return $this->result;
658
	}
659
660
	/**
661
	 * Tries to determine the port, requires given Host, User DN and Password
662
	 * @return WizardResult|false WizardResult on success, false otherwise
663
	 * @throws \Exception
664
	 */
665
	public function guessPortAndTLS() {
666
		if (!$this->checkRequirements(['ldapHost',
667
		])) {
668
			return false;
669
		}
670
		$this->checkHost();
671
		$portSettings = $this->getPortSettingsToTry();
672
673
		if (!is_array($portSettings)) {
0 ignored issues
show
introduced by
The condition is_array($portSettings) is always true.
Loading history...
674
			throw new \Exception(print_r($portSettings, true));
675
		}
676
677
		//proceed from the best configuration and return on first success
678
		foreach ($portSettings as $setting) {
679
			$p = $setting['port'];
680
			$t = $setting['tls'];
681
			$this->logger->debug(
682
				'Wiz: trying port '. $p . ', TLS '. $t,
683
				['app' => 'user_ldap']
684
			);
685
			//connectAndBind may throw Exception, it needs to be catched by the
686
			//callee of this method
687
688
			try {
689
				$settingsFound = $this->connectAndBind($p, $t);
690
			} catch (\Exception $e) {
691
				// any reply other than -1 (= cannot connect) is already okay,
692
				// because then we found the server
693
				// unavailable startTLS returns -11
694
				if ($e->getCode() > 0) {
695
					$settingsFound = true;
696
				} else {
697
					throw $e;
698
				}
699
			}
700
701
			if ($settingsFound === true) {
702
				$config = [
703
					'ldapPort' => $p,
704
					'ldapTLS' => (int)$t
705
				];
706
				$this->configuration->setConfiguration($config);
707
				$this->logger->debug(
708
					'Wiz: detected Port ' . $p,
709
					['app' => 'user_ldap']
710
				);
711
				$this->result->addChange('ldap_port', $p);
712
				return $this->result;
713
			}
714
		}
715
716
		//custom port, undetected (we do not brute force)
717
		return false;
718
	}
719
720
	/**
721
	 * tries to determine a base dn from User DN or LDAP Host
722
	 * @return WizardResult|false WizardResult on success, false otherwise
723
	 */
724
	public function guessBaseDN() {
725
		if (!$this->checkRequirements(['ldapHost',
726
			'ldapPort',
727
		])) {
728
			return false;
729
		}
730
731
		//check whether a DN is given in the agent name (99.9% of all cases)
732
		$base = null;
733
		$i = stripos($this->configuration->ldapAgentName, 'dc=');
0 ignored issues
show
Bug Best Practice introduced by
The property ldapAgentName does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
734
		if ($i !== false) {
735
			$base = substr($this->configuration->ldapAgentName, $i);
736
			if ($this->testBaseDN($base)) {
737
				$this->applyFind('ldap_base', $base);
738
				return $this->result;
739
			}
740
		}
741
742
		//this did not help :(
743
		//Let's see whether we can parse the Host URL and convert the domain to
744
		//a base DN
745
		$helper = new Helper(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection());
746
		$domain = $helper->getDomainFromURL($this->configuration->ldapHost);
0 ignored issues
show
Bug Best Practice introduced by
The property ldapHost does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
747
		if (!$domain) {
748
			return false;
749
		}
750
751
		$dparts = explode('.', $domain);
752
		while (count($dparts) > 0) {
753
			$base2 = 'dc=' . implode(',dc=', $dparts);
754
			if ($base !== $base2 && $this->testBaseDN($base2)) {
755
				$this->applyFind('ldap_base', $base2);
756
				return $this->result;
757
			}
758
			array_shift($dparts);
759
		}
760
761
		return false;
762
	}
763
764
	/**
765
	 * sets the found value for the configuration key in the WizardResult
766
	 * as well as in the Configuration instance
767
	 * @param string $key the configuration key
768
	 * @param string $value the (detected) value
769
	 *
770
	 */
771
	private function applyFind($key, $value) {
772
		$this->result->addChange($key, $value);
773
		$this->configuration->setConfiguration([$key => $value]);
774
	}
775
776
	/**
777
	 * Checks, whether a port was entered in the Host configuration
778
	 * field. In this case the port will be stripped off, but also stored as
779
	 * setting.
780
	 */
781
	private function checkHost() {
782
		$host = $this->configuration->ldapHost;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapHost does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
783
		$hostInfo = parse_url($host);
784
785
		//removes Port from Host
786
		if (is_array($hostInfo) && isset($hostInfo['port'])) {
787
			$port = $hostInfo['port'];
788
			$host = str_replace(':'.$port, '', $host);
789
			$this->applyFind('ldap_host', $host);
790
			$this->applyFind('ldap_port', $port);
791
		}
792
	}
793
794
	/**
795
	 * tries to detect the group member association attribute which is
796
	 * one of 'uniqueMember', 'memberUid', 'member', 'gidNumber'
797
	 * @return string|false, string with the attribute name, false on error
798
	 * @throws \Exception
799
	 */
800
	private function detectGroupMemberAssoc() {
801
		$possibleAttrs = ['uniqueMember', 'memberUid', 'member', 'gidNumber', 'zimbraMailForwardingAddress'];
802
		$filter = $this->configuration->ldapGroupFilter;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapGroupFilter does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
803
		if (empty($filter)) {
804
			return false;
805
		}
806
		$cr = $this->getConnection();
807
		if (!$cr) {
808
			throw new \Exception('Could not connect to LDAP');
809
		}
810
		$base = $this->configuration->ldapBaseGroups[0] ?: $this->configuration->ldapBase[0];
0 ignored issues
show
Bug Best Practice introduced by
The property ldapBase does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property ldapBaseGroups does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
811
		$rr = $this->ldap->search($cr, $base, $filter, $possibleAttrs, 0, 1000);
812
		if (!$this->ldap->isResource($rr)) {
813
			return false;
814
		}
815
		$er = $this->ldap->firstEntry($cr, $rr);
816
		while ($this->ldap->isResource($er)) {
817
			$this->ldap->getDN($cr, $er);
818
			$attrs = $this->ldap->getAttributes($cr, $er);
819
			$result = [];
820
			$possibleAttrsCount = count($possibleAttrs);
821
			for ($i = 0; $i < $possibleAttrsCount; $i++) {
822
				if (isset($attrs[$possibleAttrs[$i]])) {
823
					$result[$possibleAttrs[$i]] = $attrs[$possibleAttrs[$i]]['count'];
824
				}
825
			}
826
			if (!empty($result)) {
827
				natsort($result);
828
				return key($result);
829
			}
830
831
			$er = $this->ldap->nextEntry($cr, $er);
832
		}
833
834
		return false;
835
	}
836
837
	/**
838
	 * Checks whether for a given BaseDN results will be returned
839
	 * @param string $base the BaseDN to test
840
	 * @return bool true on success, false otherwise
841
	 * @throws \Exception
842
	 */
843
	private function testBaseDN($base) {
844
		$cr = $this->getConnection();
845
		if (!$cr) {
846
			throw new \Exception('Could not connect to LDAP');
847
		}
848
849
		//base is there, let's validate it. If we search for anything, we should
850
		//get a result set > 0 on a proper base
851
		$rr = $this->ldap->search($cr, $base, 'objectClass=*', ['dn'], 0, 1);
852
		if (!$this->ldap->isResource($rr)) {
853
			$errorNo = $this->ldap->errno($cr);
854
			$errorMsg = $this->ldap->error($cr);
855
			$this->logger->info(
856
				'Wiz: Could not search base '.$base.' Error '.$errorNo.': '.$errorMsg,
857
				['app' => 'user_ldap']
858
			);
859
			return false;
860
		}
861
		$entries = $this->ldap->countEntries($cr, $rr);
862
		return ($entries !== false) && ($entries > 0);
863
	}
864
865
	/**
866
	 * Checks whether the server supports memberOf in LDAP Filter.
867
	 * Note: at least in OpenLDAP, availability of memberOf is dependent on
868
	 * a configured objectClass. I.e. not necessarily for all available groups
869
	 * memberOf does work.
870
	 *
871
	 * @return bool true if it does, false otherwise
872
	 * @throws \Exception
873
	 */
874
	private function testMemberOf() {
875
		$cr = $this->getConnection();
876
		if (!$cr) {
877
			throw new \Exception('Could not connect to LDAP');
878
		}
879
		$result = $this->access->countUsers('memberOf=*', ['memberOf'], 1);
880
		if (is_int($result) && $result > 0) {
881
			return true;
882
		}
883
		return false;
884
	}
885
886
	/**
887
	 * creates an LDAP Filter from given configuration
888
	 * @param integer $filterType int, for which use case the filter shall be created
889
	 * can be any of self::LFILTER_USER_LIST, self::LFILTER_LOGIN or
890
	 * self::LFILTER_GROUP_LIST
891
	 * @return string|false string with the filter on success, false otherwise
892
	 * @throws \Exception
893
	 */
894
	private function composeLdapFilter($filterType) {
895
		$filter = '';
896
		$parts = 0;
897
		switch ($filterType) {
898
			case self::LFILTER_USER_LIST:
899
				$objcs = $this->configuration->ldapUserFilterObjectclass;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapUserFilterObjectclass does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
900
				//glue objectclasses
901
				if (is_array($objcs) && count($objcs) > 0) {
902
					$filter .= '(|';
903
					foreach ($objcs as $objc) {
904
						$filter .= '(objectclass=' . $objc . ')';
905
					}
906
					$filter .= ')';
907
					$parts++;
908
				}
909
				//glue group memberships
910
				if ($this->configuration->hasMemberOfFilterSupport) {
0 ignored issues
show
Bug Best Practice introduced by
The property hasMemberOfFilterSupport does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
911
					$cns = $this->configuration->ldapUserFilterGroups;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapUserFilterGroups does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
912
					if (is_array($cns) && count($cns) > 0) {
913
						$filter .= '(|';
914
						$cr = $this->getConnection();
915
						if (!$cr) {
916
							throw new \Exception('Could not connect to LDAP');
917
						}
918
						$base = $this->configuration->ldapBase[0];
0 ignored issues
show
Bug Best Practice introduced by
The property ldapBase does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
919
						foreach ($cns as $cn) {
920
							$rr = $this->ldap->search($cr, $base, 'cn=' . $cn, ['dn', 'primaryGroupToken']);
921
							if (!$this->ldap->isResource($rr)) {
922
								continue;
923
							}
924
							$er = $this->ldap->firstEntry($cr, $rr);
925
							$attrs = $this->ldap->getAttributes($cr, $er);
926
							$dn = $this->ldap->getDN($cr, $er);
927
							if ($dn === false || $dn === '') {
928
								continue;
929
							}
930
							$filterPart = '(memberof=' . $dn . ')';
931
							if (isset($attrs['primaryGroupToken'])) {
932
								$pgt = $attrs['primaryGroupToken'][0];
933
								$primaryFilterPart = '(primaryGroupID=' . $pgt .')';
934
								$filterPart = '(|' . $filterPart . $primaryFilterPart . ')';
935
							}
936
							$filter .= $filterPart;
937
						}
938
						$filter .= ')';
939
					}
940
					$parts++;
941
				}
942
				//wrap parts in AND condition
943
				if ($parts > 1) {
944
					$filter = '(&' . $filter . ')';
945
				}
946
				if ($filter === '') {
947
					$filter = '(objectclass=*)';
948
				}
949
				break;
950
951
			case self::LFILTER_GROUP_LIST:
952
				$objcs = $this->configuration->ldapGroupFilterObjectclass;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapGroupFilterObjectclass does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
953
				//glue objectclasses
954
				if (is_array($objcs) && count($objcs) > 0) {
955
					$filter .= '(|';
956
					foreach ($objcs as $objc) {
957
						$filter .= '(objectclass=' . $objc . ')';
958
					}
959
					$filter .= ')';
960
					$parts++;
961
				}
962
				//glue group memberships
963
				$cns = $this->configuration->ldapGroupFilterGroups;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapGroupFilterGroups does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
964
				if (is_array($cns) && count($cns) > 0) {
965
					$filter .= '(|';
966
					foreach ($cns as $cn) {
967
						$filter .= '(cn=' . $cn . ')';
968
					}
969
					$filter .= ')';
970
				}
971
				$parts++;
972
				//wrap parts in AND condition
973
				if ($parts > 1) {
974
					$filter = '(&' . $filter . ')';
975
				}
976
				break;
977
978
			case self::LFILTER_LOGIN:
979
				$ulf = $this->configuration->ldapUserFilter;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapUserFilter does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
980
				$loginpart = '=%uid';
981
				$filterUsername = '';
982
				$userAttributes = $this->getUserAttributes();
983
				$userAttributes = array_change_key_case(array_flip($userAttributes));
984
				$parts = 0;
985
986
				if ($this->configuration->ldapLoginFilterUsername === '1') {
0 ignored issues
show
Bug Best Practice introduced by
The property ldapLoginFilterUsername does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
987
					$attr = '';
988
					if (isset($userAttributes['uid'])) {
989
						$attr = 'uid';
990
					} elseif (isset($userAttributes['samaccountname'])) {
991
						$attr = 'samaccountname';
992
					} elseif (isset($userAttributes['cn'])) {
993
						//fallback
994
						$attr = 'cn';
995
					}
996
					if ($attr !== '') {
997
						$filterUsername = '(' . $attr . $loginpart . ')';
998
						$parts++;
999
					}
1000
				}
1001
1002
				$filterEmail = '';
1003
				if ($this->configuration->ldapLoginFilterEmail === '1') {
0 ignored issues
show
Bug Best Practice introduced by
The property ldapLoginFilterEmail does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
1004
					$filterEmail = '(|(mailPrimaryAddress=%uid)(mail=%uid))';
1005
					$parts++;
1006
				}
1007
1008
				$filterAttributes = '';
1009
				$attrsToFilter = $this->configuration->ldapLoginFilterAttributes;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapLoginFilterAttributes does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
1010
				if (is_array($attrsToFilter) && count($attrsToFilter) > 0) {
1011
					$filterAttributes = '(|';
1012
					foreach ($attrsToFilter as $attribute) {
1013
						$filterAttributes .= '(' . $attribute . $loginpart . ')';
1014
					}
1015
					$filterAttributes .= ')';
1016
					$parts++;
1017
				}
1018
1019
				$filterLogin = '';
1020
				if ($parts > 1) {
1021
					$filterLogin = '(|';
1022
				}
1023
				$filterLogin .= $filterUsername;
1024
				$filterLogin .= $filterEmail;
1025
				$filterLogin .= $filterAttributes;
1026
				if ($parts > 1) {
1027
					$filterLogin .= ')';
1028
				}
1029
1030
				$filter = '(&'.$ulf.$filterLogin.')';
1031
				break;
1032
		}
1033
1034
		$this->logger->debug(
1035
			'Wiz: Final filter '.$filter,
1036
			['app' => 'user_ldap']
1037
		);
1038
1039
		return $filter;
1040
	}
1041
1042
	/**
1043
	 * Connects and Binds to an LDAP Server
1044
	 *
1045
	 * @param int $port the port to connect with
1046
	 * @param bool $tls whether startTLS is to be used
1047
	 * @return bool
1048
	 * @throws \Exception
1049
	 */
1050
	private function connectAndBind($port, $tls) {
1051
		//connect, does not really trigger any server communication
1052
		$host = $this->configuration->ldapHost;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapHost does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
1053
		$hostInfo = parse_url($host);
1054
		if (!$hostInfo) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $hostInfo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1055
			throw new \Exception(self::$l->t('Invalid Host'));
1056
		}
1057
		$this->logger->debug(
1058
			'Wiz: Attempting to connect',
1059
			['app' => 'user_ldap']
1060
		);
1061
		$cr = $this->ldap->connect($host, $port);
1062
		if (!$this->ldap->isResource($cr)) {
1063
			throw new \Exception(self::$l->t('Invalid Host'));
1064
		}
1065
1066
		//set LDAP options
1067
		$this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
1068
		$this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
1069
		$this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
1070
1071
		try {
1072
			if ($tls) {
1073
				$isTlsWorking = @$this->ldap->startTls($cr);
1074
				if (!$isTlsWorking) {
1075
					return false;
1076
				}
1077
			}
1078
1079
			$this->logger->debug(
1080
				'Wiz: Attemping to Bind',
1081
				['app' => 'user_ldap']
1082
			);
1083
			//interesting part: do the bind!
1084
			$login = $this->ldap->bind($cr,
1085
				$this->configuration->ldapAgentName,
0 ignored issues
show
Bug Best Practice introduced by
The property ldapAgentName does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
1086
				$this->configuration->ldapAgentPassword
0 ignored issues
show
Bug Best Practice introduced by
The property ldapAgentPassword does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
1087
			);
1088
			$errNo = $this->ldap->errno($cr);
1089
			$error = ldap_error($cr);
1090
			$this->ldap->unbind($cr);
1091
		} catch (ServerNotAvailableException $e) {
1092
			return false;
1093
		}
1094
1095
		if ($login === true) {
1096
			$this->ldap->unbind($cr);
1097
			$this->logger->debug(
1098
				'Wiz: Bind successful to Port '. $port . ' TLS ' . (int)$tls,
1099
				['app' => 'user_ldap']
1100
			);
1101
			return true;
1102
		}
1103
1104
		if ($errNo === -1) {
1105
			//host, port or TLS wrong
1106
			return false;
1107
		}
1108
		throw new \Exception($error, $errNo);
1109
	}
1110
1111
	/**
1112
	 * checks whether a valid combination of agent and password has been
1113
	 * provided (either two values or nothing for anonymous connect)
1114
	 * @return bool, true if everything is fine, false otherwise
1115
	 */
1116
	private function checkAgentRequirements() {
1117
		$agent = $this->configuration->ldapAgentName;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapAgentName does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
1118
		$pwd = $this->configuration->ldapAgentPassword;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapAgentPassword does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
1119
1120
		return
1121
			($agent !== '' && $pwd !== '')
1122
			|| ($agent === '' && $pwd === '')
1123
		;
1124
	}
1125
1126
	/**
1127
	 * @param array $reqs
1128
	 * @return bool
1129
	 */
1130
	private function checkRequirements($reqs) {
1131
		$this->checkAgentRequirements();
1132
		foreach ($reqs as $option) {
1133
			$value = $this->configuration->$option;
1134
			if (empty($value)) {
1135
				return false;
1136
			}
1137
		}
1138
		return true;
1139
	}
1140
1141
	/**
1142
	 * does a cumulativeSearch on LDAP to get different values of a
1143
	 * specified attribute
1144
	 * @param string[] $filters array, the filters that shall be used in the search
1145
	 * @param string $attr the attribute of which a list of values shall be returned
1146
	 * @param int $dnReadLimit the amount of how many DNs should be analyzed.
1147
	 * The lower, the faster
1148
	 * @param string $maxF string. if not null, this variable will have the filter that
1149
	 * yields most result entries
1150
	 * @return array|false an array with the values on success, false otherwise
1151
	 */
1152
	public function cumulativeSearchOnAttribute($filters, $attr, $dnReadLimit = 3, &$maxF = null) {
1153
		$dnRead = [];
1154
		$foundItems = [];
1155
		$maxEntries = 0;
1156
		if (!is_array($this->configuration->ldapBase)
0 ignored issues
show
Bug Best Practice introduced by
The property ldapBase does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
1157
		   || !isset($this->configuration->ldapBase[0])) {
1158
			return false;
1159
		}
1160
		$base = $this->configuration->ldapBase[0];
1161
		$cr = $this->getConnection();
1162
		if (!$this->ldap->isResource($cr)) {
1163
			return false;
1164
		}
1165
		$lastFilter = null;
1166
		if (isset($filters[count($filters) - 1])) {
1167
			$lastFilter = $filters[count($filters) - 1];
1168
		}
1169
		foreach ($filters as $filter) {
1170
			if ($lastFilter === $filter && count($foundItems) > 0) {
1171
				//skip when the filter is a wildcard and results were found
1172
				continue;
1173
			}
1174
			// 20k limit for performance and reason
1175
			$rr = $this->ldap->search($cr, $base, $filter, [$attr], 0, 20000);
1176
			if (!$this->ldap->isResource($rr)) {
1177
				continue;
1178
			}
1179
			$entries = $this->ldap->countEntries($cr, $rr);
1180
			$getEntryFunc = 'firstEntry';
1181
			if (($entries !== false) && ($entries > 0)) {
1182
				if (!is_null($maxF) && $entries > $maxEntries) {
1183
					$maxEntries = $entries;
1184
					$maxF = $filter;
1185
				}
1186
				$dnReadCount = 0;
1187
				do {
1188
					$entry = $this->ldap->$getEntryFunc($cr, $rr);
1189
					$getEntryFunc = 'nextEntry';
1190
					if (!$this->ldap->isResource($entry)) {
1191
						continue 2;
1192
					}
1193
					$rr = $entry; //will be expected by nextEntry next round
1194
					$attributes = $this->ldap->getAttributes($cr, $entry);
1195
					$dn = $this->ldap->getDN($cr, $entry);
1196
					if ($dn === false || in_array($dn, $dnRead)) {
1197
						continue;
1198
					}
1199
					$newItems = [];
1200
					$state = $this->getAttributeValuesFromEntry($attributes,
1201
																$attr,
1202
																$newItems);
1203
					$dnReadCount++;
1204
					$foundItems = array_merge($foundItems, $newItems);
1205
					$this->resultCache[$dn][$attr] = $newItems;
1206
					$dnRead[] = $dn;
1207
				} while (($state === self::LRESULT_PROCESSED_SKIP
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $state does not seem to be defined for all execution paths leading up to this point.
Loading history...
1208
						|| $this->ldap->isResource($entry))
1209
						&& ($dnReadLimit === 0 || $dnReadCount < $dnReadLimit));
1210
			}
1211
		}
1212
1213
		return array_unique($foundItems);
1214
	}
1215
1216
	/**
1217
	 * determines if and which $attr are available on the LDAP server
1218
	 * @param string[] $objectclasses the objectclasses to use as search filter
1219
	 * @param string $attr the attribute to look for
1220
	 * @param string $dbkey the dbkey of the setting the feature is connected to
1221
	 * @param string $confkey the confkey counterpart for the $dbkey as used in the
1222
	 * Configuration class
1223
	 * @param bool $po whether the objectClass with most result entries
1224
	 * shall be pre-selected via the result
1225
	 * @return array|false list of found items.
1226
	 * @throws \Exception
1227
	 */
1228
	private function determineFeature($objectclasses, $attr, $dbkey, $confkey, $po = false) {
1229
		$cr = $this->getConnection();
1230
		if (!$cr) {
1231
			throw new \Exception('Could not connect to LDAP');
1232
		}
1233
		$p = 'objectclass=';
1234
		foreach ($objectclasses as $key => $value) {
1235
			$objectclasses[$key] = $p.$value;
1236
		}
1237
		$maxEntryObjC = '';
1238
1239
		//how deep to dig?
1240
		//When looking for objectclasses, testing few entries is sufficient,
1241
		$dig = 3;
1242
1243
		$availableFeatures =
1244
			$this->cumulativeSearchOnAttribute($objectclasses, $attr,
1245
											   $dig, $maxEntryObjC);
1246
		if (is_array($availableFeatures)
1247
		   && count($availableFeatures) > 0) {
1248
			natcasesort($availableFeatures);
1249
			//natcasesort keeps indices, but we must get rid of them for proper
1250
			//sorting in the web UI. Therefore: array_values
1251
			$this->result->addOptions($dbkey, array_values($availableFeatures));
1252
		} else {
1253
			throw new \Exception(self::$l->t('Could not find the desired feature'));
1254
		}
1255
1256
		$setFeatures = $this->configuration->$confkey;
1257
		if (is_array($setFeatures) && !empty($setFeatures)) {
1258
			//something is already configured? pre-select it.
1259
			$this->result->addChange($dbkey, $setFeatures);
1260
		} elseif ($po && $maxEntryObjC !== '') {
1261
			//pre-select objectclass with most result entries
1262
			$maxEntryObjC = str_replace($p, '', $maxEntryObjC);
1263
			$this->applyFind($dbkey, $maxEntryObjC);
1264
			$this->result->addChange($dbkey, $maxEntryObjC);
1265
		}
1266
1267
		return $availableFeatures;
1268
	}
1269
1270
	/**
1271
	 * appends a list of values fr
1272
	 * @param array $result the return value from ldap_get_attributes
1273
	 * @param string $attribute the attribute values to look for
1274
	 * @param array &$known new values will be appended here
1275
	 * @return int, state on of the class constants LRESULT_PROCESSED_OK,
1276
	 * LRESULT_PROCESSED_INVALID or LRESULT_PROCESSED_SKIP
1277
	 */
1278
	private function getAttributeValuesFromEntry($result, $attribute, &$known) {
1279
		if (!is_array($result)
0 ignored issues
show
introduced by
The condition is_array($result) is always true.
Loading history...
1280
		   || !isset($result['count'])
1281
		   || !$result['count'] > 0) {
1282
			return self::LRESULT_PROCESSED_INVALID;
1283
		}
1284
1285
		// strtolower on all keys for proper comparison
1286
		$result = \OCP\Util::mb_array_change_key_case($result);
1287
		$attribute = strtolower($attribute);
1288
		if (isset($result[$attribute])) {
1289
			foreach ($result[$attribute] as $key => $val) {
1290
				if ($key === 'count') {
1291
					continue;
1292
				}
1293
				if (!in_array($val, $known)) {
1294
					$known[] = $val;
1295
				}
1296
			}
1297
			return self::LRESULT_PROCESSED_OK;
1298
		} else {
1299
			return self::LRESULT_PROCESSED_SKIP;
1300
		}
1301
	}
1302
1303
	/**
1304
	 * @return bool|mixed
1305
	 */
1306
	private function getConnection() {
1307
		if (!is_null($this->cr)) {
1308
			return $this->cr;
1309
		}
1310
1311
		$cr = $this->ldap->connect(
1312
			$this->configuration->ldapHost,
0 ignored issues
show
Bug Best Practice introduced by
The property ldapHost does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
1313
			$this->configuration->ldapPort
0 ignored issues
show
Bug Best Practice introduced by
The property ldapPort does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
1314
		);
1315
1316
		$this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
1317
		$this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
1318
		$this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
1319
		if ($this->configuration->ldapTLS === 1) {
0 ignored issues
show
Bug Best Practice introduced by
The property ldapTLS does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
1320
			$this->ldap->startTls($cr);
1321
		}
1322
1323
		$lo = @$this->ldap->bind($cr,
1324
								 $this->configuration->ldapAgentName,
0 ignored issues
show
Bug Best Practice introduced by
The property ldapAgentName does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
1325
								 $this->configuration->ldapAgentPassword);
0 ignored issues
show
Bug Best Practice introduced by
The property ldapAgentPassword does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
1326
		if ($lo === true) {
1327
			$this->cr = $cr;
1328
			return $cr;
1329
		}
1330
1331
		return false;
1332
	}
1333
1334
	/**
1335
	 * @return array
1336
	 */
1337
	private function getDefaultLdapPortSettings() {
1338
		static $settings = [
1339
			['port' => 7636, 'tls' => false],
1340
			['port' => 636, 'tls' => false],
1341
			['port' => 7389, 'tls' => true],
1342
			['port' => 389, 'tls' => true],
1343
			['port' => 7389, 'tls' => false],
1344
			['port' => 389, 'tls' => false],
1345
		];
1346
		return $settings;
1347
	}
1348
1349
	/**
1350
	 * @return array
1351
	 */
1352
	private function getPortSettingsToTry() {
1353
		//389 ← LDAP / Unencrypted or StartTLS
1354
		//636 ← LDAPS / SSL
1355
		//7xxx ← UCS. need to be checked first, because both ports may be open
1356
		$host = $this->configuration->ldapHost;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapHost does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
1357
		$port = (int)$this->configuration->ldapPort;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapPort does not exist on OCA\User_LDAP\Configuration. Since you implemented __get, consider adding a @property annotation.
Loading history...
1358
		$portSettings = [];
1359
1360
		//In case the port is already provided, we will check this first
1361
		if ($port > 0) {
1362
			$hostInfo = parse_url($host);
1363
			if (!(is_array($hostInfo)
1364
				&& isset($hostInfo['scheme'])
1365
				&& stripos($hostInfo['scheme'], 'ldaps') !== false)) {
1366
				$portSettings[] = ['port' => $port, 'tls' => true];
1367
			}
1368
			$portSettings[] = ['port' => $port, 'tls' => false];
1369
		}
1370
1371
		//default ports
1372
		$portSettings = array_merge($portSettings,
1373
									$this->getDefaultLdapPortSettings());
1374
1375
		return $portSettings;
1376
	}
1377
}
1378