Wizard   F
last analyzed

Complexity

Total Complexity 240

Size/Duplication

Total Lines 1337
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 704
dl 0
loc 1337
rs 1.896
c 0
b 0
f 0
wmc 240

39 Methods

Rating   Name   Duplication   Size   Complexity  
B fetchGroups() 0 45 10
A checkHost() 0 10 3
A applyFind() 0 3 1
A __destruct() 0 3 2
A checkRequirements() 0 9 3
B connectAndBind() 0 59 9
B detectGroupMemberAssoc() 0 36 9
A determineGroupsForUsers() 0 3 1
A determineUserObjectClasses() 0 25 4
B getAttributeValuesFromEntry() 0 21 7
A countInBaseDN() 0 5 1
A determineAttributes() 0 26 6
A determineGroupMemberAssoc() 0 16 4
A countUsersWithAttribute() 0 17 4
B detectEmailAttribute() 0 43 9
B detectUserDisplayNameAttribute() 0 34 8
A checkAgentRequirements() 0 7 4
A getConnection() 0 30 5
A __construct() 0 13 2
D cumulativeSearchOnAttribute() 0 65 21
A determineGroups() 0 24 6
A countGroups() 0 30 5
A testBaseDN() 0 21 4
A determineGroupsForGroups() 0 4 1
B countEntries() 0 25 7
A getGroupFilter() 0 19 4
F composeLdapFilter() 0 150 37
A getDefaultLdapPortSettings() 0 10 1
A determineGroupObjectClasses() 0 21 4
A getUserListFilter() 0 21 5
A testLoginName() 0 28 6
A getUserLoginFilter() 0 16 4
A testMemberOf() 0 10 4
B getUserAttributes() 0 31 7
A countUsers() 0 15 2
B guessPortAndTLS() 0 49 6
B guessBaseDN() 0 40 9
B determineFeature() 0 40 9
A getPortSettingsToTry() 0 26 6

How to fix   Complexity   

Complex Class

Complex classes like Wizard often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Wizard, and based on these observations, apply Extract Interface, too.

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
 * @author Vincent Van Houtte <[email protected]>
23
 * @author Côme Chilliet <[email protected]>
24
 *
25
 * @license AGPL-3.0
26
 *
27
 * This code is free software: you can redistribute it and/or modify
28
 * it under the terms of the GNU Affero General Public License, version 3,
29
 * as published by the Free Software Foundation.
30
 *
31
 * This program is distributed in the hope that it will be useful,
32
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
33
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
34
 * GNU Affero General Public License for more details.
35
 *
36
 * You should have received a copy of the GNU Affero General Public License, version 3,
37
 * along with this program. If not, see <http://www.gnu.org/licenses/>
38
 *
39
 */
40
41
namespace OCA\User_LDAP;
42
43
use OC\ServerNotAvailableException;
44
use OCP\IL10N;
45
use OCP\L10N\IFactory as IL10NFactory;
46
use Psr\Log\LoggerInterface;
47
48
class Wizard extends LDAPUtility {
49
	protected static ?IL10N $l = null;
50
	protected Access $access;
51
	/** @var resource|\LDAP\Connection|null */
0 ignored issues
show
Bug introduced by
The type LDAP\Connection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
52
	protected $cr;
53
	protected Configuration $configuration;
54
	protected WizardResult $result;
55
	protected LoggerInterface $logger;
56
57
	public const LRESULT_PROCESSED_OK = 2;
58
	public const LRESULT_PROCESSED_INVALID = 3;
59
	public const LRESULT_PROCESSED_SKIP = 4;
60
61
	public const LFILTER_LOGIN = 2;
62
	public const LFILTER_USER_LIST = 3;
63
	public const LFILTER_GROUP_LIST = 4;
64
65
	public const LFILTER_MODE_ASSISTED = 2;
66
	public const LFILTER_MODE_RAW = 1;
67
68
	public const LDAP_NW_TIMEOUT = 4;
69
70
	public function __construct(
71
		Configuration $configuration,
72
		ILDAPWrapper $ldap,
73
		Access $access
74
	) {
75
		parent::__construct($ldap);
76
		$this->configuration = $configuration;
77
		if (is_null(static::$l)) {
78
			static::$l = \OC::$server->get(IL10NFactory::class)->get('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
	 * @throws \Exception
97
	 */
98
	public function countEntries(string $filter, string $type): int {
99
		$reqs = ['ldapHost', 'ldapBase'];
100
		if (!$this->configuration->usesLdapi()) {
101
			$reqs[] = 'ldapPort';
102
		}
103
		if ($type === 'users') {
104
			$reqs[] = 'ldapUserFilter';
105
		}
106
		if (!$this->checkRequirements($reqs)) {
107
			throw new \Exception('Requirements not met', 400);
108
		}
109
110
		$attr = ['dn']; // default
111
		$limit = 1001;
112
		if ($type === 'groups') {
113
			$result = $this->access->countGroups($filter, $attr, $limit);
114
		} elseif ($type === 'users') {
115
			$result = $this->access->countUsers($filter, $attr, $limit);
116
		} elseif ($type === 'objects') {
117
			$result = $this->access->countObjects($limit);
118
		} else {
119
			throw new \Exception('Internal error: Invalid object type', 500);
120
		}
121
122
		return (int)$result;
123
	}
124
125
	/**
126
	 * @return WizardResult|false
127
	 */
128
	public function countGroups() {
129
		$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...
130
131
		if (empty($filter)) {
132
			$output = self::$l->n('%n group found', '%n groups found', 0);
0 ignored issues
show
Bug introduced by
The method n() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

132
			/** @scrutinizer ignore-call */ 
133
   $output = self::$l->n('%n group found', '%n groups found', 0);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
133
			$this->result->addChange('ldap_group_count', $output);
134
			return $this->result;
135
		}
136
137
		try {
138
			$groupsTotal = $this->countEntries($filter, 'groups');
139
		} catch (\Exception $e) {
140
			//400 can be ignored, 500 is forwarded
141
			if ($e->getCode() === 500) {
142
				throw $e;
143
			}
144
			return false;
145
		}
146
147
		if ($groupsTotal > 1000) {
148
			$output = self::$l->t('> 1000 groups found');
149
		} else {
150
			$output = self::$l->n(
151
				'%n group found',
152
				'%n groups found',
153
				$groupsTotal
154
			);
155
		}
156
		$this->result->addChange('ldap_group_count', $output);
157
		return $this->result;
158
	}
159
160
	/**
161
	 * @throws \Exception
162
	 */
163
	public function countUsers(): WizardResult {
164
		$filter = $this->access->getFilterForUserCount();
165
166
		$usersTotal = $this->countEntries($filter, 'users');
167
		if ($usersTotal > 1000) {
168
			$output = self::$l->t('> 1000 users found');
169
		} else {
170
			$output = self::$l->n(
171
				'%n user found',
172
				'%n users found',
173
				$usersTotal
174
			);
175
		}
176
		$this->result->addChange('ldap_user_count', $output);
177
		return $this->result;
178
	}
179
180
	/**
181
	 * counts any objects in the currently set base dn
182
	 *
183
	 * @throws \Exception
184
	 */
185
	public function countInBaseDN(): WizardResult {
186
		// we don't need to provide a filter in this case
187
		$total = $this->countEntries('', 'objects');
188
		$this->result->addChange('ldap_test_base', $total);
189
		return $this->result;
190
	}
191
192
	/**
193
	 * counts users with a specified attribute
194
	 * @return int|false
195
	 */
196
  public function countUsersWithAttribute(string $attr, bool $existsCheck = false) {
197
		$reqs = ['ldapHost', 'ldapBase', 'ldapUserFilter'];
198
		if (!$this->configuration->usesLdapi()) {
199
			$reqs[] = 'ldapPort';
200
		}
201
		if (!$this->checkRequirements($reqs)) {
202
			return  false;
203
		}
204
205
		$filter = $this->access->combineFilterWithAnd([
206
			$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...
207
			$attr . '=*'
208
		]);
209
210
		$limit = $existsCheck ? null : 1;
211
212
		return $this->access->countUsers($filter, ['dn'], $limit);
213
	}
214
215
	/**
216
	 * detects the display name attribute. If a setting is already present that
217
	 * returns at least one hit, the detection will be canceled.
218
	 * @return WizardResult|false
219
	 * @throws \Exception
220
	 */
221
	public function detectUserDisplayNameAttribute() {
222
		$reqs = ['ldapHost', 'ldapBase', 'ldapUserFilter'];
223
		if (!$this->configuration->usesLdapi()) {
224
			$reqs[] = 'ldapPort';
225
		}
226
		if (!$this->checkRequirements($reqs)) {
227
			return  false;
228
		}
229
230
		$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...
231
		if ($attr !== '' && $attr !== 'displayName') {
232
			// most likely not the default value with upper case N,
233
			// verify it still produces a result
234
			$count = (int)$this->countUsersWithAttribute($attr, true);
235
			if ($count > 0) {
236
				//no change, but we sent it back to make sure the user interface
237
				//is still correct, even if the ajax call was cancelled meanwhile
238
				$this->result->addChange('ldap_display_name', $attr);
239
				return $this->result;
240
			}
241
		}
242
243
		// first attribute that has at least one result wins
244
		$displayNameAttrs = ['displayname', 'cn'];
245
		foreach ($displayNameAttrs as $attr) {
246
			$count = (int)$this->countUsersWithAttribute($attr, true);
247
248
			if ($count > 0) {
249
				$this->applyFind('ldap_display_name', $attr);
250
				return $this->result;
251
			}
252
		}
253
254
		throw new \Exception(self::$l->t('Could not detect user display name attribute. Please specify it yourself in advanced LDAP settings.'));
255
	}
256
257
	/**
258
	 * detects the most often used email attribute for users applying to the
259
	 * user list filter. If a setting is already present that returns at least
260
	 * one hit, the detection will be canceled.
261
	 * @return WizardResult|bool
262
	 */
263
	public function detectEmailAttribute() {
264
		$reqs = ['ldapHost', 'ldapBase', 'ldapUserFilter'];
265
		if (!$this->configuration->usesLdapi()) {
266
			$reqs[] = 'ldapPort';
267
		}
268
		if (!$this->checkRequirements($reqs)) {
269
			return  false;
270
		}
271
272
		$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...
273
		if ($attr !== '') {
274
			$count = (int)$this->countUsersWithAttribute($attr, true);
275
			if ($count > 0) {
276
				return false;
277
			}
278
			$writeLog = true;
279
		} else {
280
			$writeLog = false;
281
		}
282
283
		$emailAttributes = ['mail', 'mailPrimaryAddress'];
284
		$winner = '';
285
		$maxUsers = 0;
286
		foreach ($emailAttributes as $attr) {
287
			$count = $this->countUsersWithAttribute($attr);
288
			if ($count > $maxUsers) {
289
				$maxUsers = $count;
290
				$winner = $attr;
291
			}
292
		}
293
294
		if ($winner !== '') {
295
			$this->applyFind('ldap_email_attr', $winner);
296
			if ($writeLog) {
297
				$this->logger->info(
298
					'The mail attribute has automatically been reset, '.
299
					'because the original value did not return any results.',
300
					['app' => 'user_ldap']
301
				);
302
			}
303
		}
304
305
		return $this->result;
306
	}
307
308
	/**
309
	 * @return WizardResult|false
310
	 * @throws \Exception
311
	 */
312
	public function determineAttributes() {
313
		$reqs = ['ldapHost', 'ldapBase', 'ldapUserFilter'];
314
		if (!$this->configuration->usesLdapi()) {
315
			$reqs[] = 'ldapPort';
316
		}
317
		if (!$this->checkRequirements($reqs)) {
318
			return  false;
319
		}
320
321
		$attributes = $this->getUserAttributes();
322
323
		if (!is_array($attributes)) {
324
			throw new \Exception('Failed to determine user attributes');
325
		}
326
327
		natcasesort($attributes);
328
		$attributes = array_values($attributes);
329
330
		$this->result->addOptions('ldap_loginfilter_attributes', $attributes);
331
332
		$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...
333
		if (is_array($selected) && !empty($selected)) {
334
			$this->result->addChange('ldap_loginfilter_attributes', $selected);
335
		}
336
337
		return $this->result;
338
	}
339
340
	/**
341
	 * detects the available LDAP attributes
342
	 * @return array|false
343
	 * @throws \Exception
344
	 */
345
	private function getUserAttributes() {
346
		$reqs = ['ldapHost', 'ldapBase', 'ldapUserFilter'];
347
		if (!$this->configuration->usesLdapi()) {
348
			$reqs[] = 'ldapPort';
349
		}
350
		if (!$this->checkRequirements($reqs)) {
351
			return  false;
352
		}
353
		$cr = $this->getConnection();
354
		if (!$cr) {
355
			throw new \Exception('Could not connect to LDAP');
356
		}
357
358
		$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...
359
		$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...
360
		$rr = $this->ldap->search($cr, $base, $filter, [], 1, 1);
361
		if (!$this->ldap->isResource($rr)) {
362
			return false;
363
		}
364
		/** @var resource|\LDAP\Result $rr */
365
		$er = $this->ldap->firstEntry($cr, $rr);
366
		$attributes = $this->ldap->getAttributes($cr, $er);
367
		if ($attributes === false) {
368
			return false;
369
		}
370
		$pureAttributes = [];
371
		for ($i = 0; $i < $attributes['count']; $i++) {
372
			$pureAttributes[] = $attributes[$i];
373
		}
374
375
		return $pureAttributes;
376
	}
377
378
	/**
379
	 * detects the available LDAP groups
380
	 * @return WizardResult|false the instance's WizardResult instance
381
	 */
382
	public function determineGroupsForGroups() {
383
		return $this->determineGroups('ldap_groupfilter_groups',
384
			'ldapGroupFilterGroups',
385
			false);
386
	}
387
388
	/**
389
	 * detects the available LDAP groups
390
	 * @return WizardResult|false the instance's WizardResult instance
391
	 */
392
	public function determineGroupsForUsers() {
393
		return $this->determineGroups('ldap_userfilter_groups',
394
			'ldapUserFilterGroups');
395
	}
396
397
	/**
398
	 * detects the available LDAP groups
399
	 * @return WizardResult|false the instance's WizardResult instance
400
	 * @throws \Exception
401
	 */
402
  private function determineGroups(string $dbKey, string $confKey, bool $testMemberOf = true) {
403
		$reqs = ['ldapHost', 'ldapBase'];
404
		if (!$this->configuration->usesLdapi()) {
405
			$reqs[] = 'ldapPort';
406
		}
407
		if (!$this->checkRequirements($reqs)) {
408
			return  false;
409
		}
410
		$cr = $this->getConnection();
411
		if (!$cr) {
412
			throw new \Exception('Could not connect to LDAP');
413
		}
414
415
		$this->fetchGroups($dbKey, $confKey);
416
417
		if ($testMemberOf) {
418
			$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...
419
			$this->result->markChange();
420
			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...
421
				throw new \Exception('memberOf is not supported by the server');
422
			}
423
		}
424
425
		return $this->result;
426
	}
427
428
	/**
429
	 * fetches all groups from LDAP and adds them to the result object
430
	 *
431
	 * @throws \Exception
432
	 */
433
	public function fetchGroups(string $dbKey, string $confKey): array {
434
		$obclasses = ['posixGroup', 'group', 'zimbraDistributionList', 'groupOfNames', 'groupOfUniqueNames'];
435
436
		$filterParts = [];
437
		foreach ($obclasses as $obclass) {
438
			$filterParts[] = 'objectclass='.$obclass;
439
		}
440
		//we filter for everything
441
		//- that looks like a group and
442
		//- has the group display name set
443
		$filter = $this->access->combineFilterWithOr($filterParts);
444
		$filter = $this->access->combineFilterWithAnd([$filter, 'cn=*']);
445
446
		$groupNames = [];
447
		$groupEntries = [];
448
		$limit = 400;
449
		$offset = 0;
450
		do {
451
			// we need to request dn additionally here, otherwise memberOf
452
			// detection will fail later
453
			$result = $this->access->searchGroups($filter, ['cn', 'dn'], $limit, $offset);
454
			foreach ($result as $item) {
455
				if (!isset($item['cn']) || !is_array($item['cn']) || !isset($item['cn'][0])) {
456
					// just in case - no issue known
457
					continue;
458
				}
459
				$groupNames[] = $item['cn'][0];
460
				$groupEntries[] = $item;
461
			}
462
			$offset += $limit;
463
		} while ($this->access->hasMoreResults());
464
465
		if (count($groupNames) > 0) {
466
			natsort($groupNames);
467
			$this->result->addOptions($dbKey, array_values($groupNames));
468
		} else {
469
			throw new \Exception(self::$l->t('Could not find the desired feature'));
470
		}
471
472
		$setFeatures = $this->configuration->$confKey;
473
		if (is_array($setFeatures) && !empty($setFeatures)) {
474
			//something is already configured? pre-select it.
475
			$this->result->addChange($dbKey, $setFeatures);
476
		}
477
		return $groupEntries;
478
	}
479
480
	/**
481
	 * @return WizardResult|false
482
	 */
483
	public function determineGroupMemberAssoc() {
484
		$reqs = ['ldapHost', 'ldapGroupFilter'];
485
		if (!$this->configuration->usesLdapi()) {
486
			$reqs[] = 'ldapPort';
487
		}
488
		if (!$this->checkRequirements($reqs)) {
489
			return  false;
490
		}
491
		$attribute = $this->detectGroupMemberAssoc();
492
		if ($attribute === false) {
493
			return false;
494
		}
495
		$this->configuration->setConfiguration(['ldapGroupMemberAssocAttr' => $attribute]);
496
		$this->result->addChange('ldap_group_member_assoc_attribute', $attribute);
497
498
		return $this->result;
499
	}
500
501
	/**
502
	 * Detects the available object classes
503
	 * @return WizardResult|false the instance's WizardResult instance
504
	 * @throws \Exception
505
	 */
506
	public function determineGroupObjectClasses() {
507
		$reqs = ['ldapHost', 'ldapBase'];
508
		if (!$this->configuration->usesLdapi()) {
509
			$reqs[] = 'ldapPort';
510
		}
511
		if (!$this->checkRequirements($reqs)) {
512
			return  false;
513
		}
514
		$cr = $this->getConnection();
515
		if (!$cr) {
516
			throw new \Exception('Could not connect to LDAP');
517
		}
518
519
		$obclasses = ['groupOfNames', 'groupOfUniqueNames', 'group', 'posixGroup', '*'];
520
		$this->determineFeature($obclasses,
521
			'objectclass',
522
			'ldap_groupfilter_objectclass',
523
			'ldapGroupFilterObjectclass',
524
			false);
525
526
		return $this->result;
527
	}
528
529
	/**
530
	 * detects the available object classes
531
	 * @return WizardResult|false
532
	 * @throws \Exception
533
	 */
534
	public function determineUserObjectClasses() {
535
		$reqs = ['ldapHost', 'ldapBase'];
536
		if (!$this->configuration->usesLdapi()) {
537
			$reqs[] = 'ldapPort';
538
		}
539
		if (!$this->checkRequirements($reqs)) {
540
			return  false;
541
		}
542
		$cr = $this->getConnection();
543
		if (!$cr) {
544
			throw new \Exception('Could not connect to LDAP');
545
		}
546
547
		$obclasses = ['inetOrgPerson', 'person', 'organizationalPerson',
548
			'user', 'posixAccount', '*'];
549
		$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...
550
		//if filter is empty, it is probably the first time the wizard is called
551
		//then, apply suggestions.
552
		$this->determineFeature($obclasses,
553
			'objectclass',
554
			'ldap_userfilter_objectclass',
555
			'ldapUserFilterObjectclass',
556
			empty($filter));
557
558
		return $this->result;
559
	}
560
561
	/**
562
	 * @return WizardResult|false
563
	 * @throws \Exception
564
	 */
565
	public function getGroupFilter() {
566
		$reqs = ['ldapHost', 'ldapBase'];
567
		if (!$this->configuration->usesLdapi()) {
568
			$reqs[] = 'ldapPort';
569
		}
570
		if (!$this->checkRequirements($reqs)) {
571
			return false;
572
		}
573
		//make sure the use display name is set
574
		$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...
575
		if ($displayName === '') {
576
			$d = $this->configuration->getDefaults();
577
			$this->applyFind('ldap_group_display_name',
578
				$d['ldap_group_display_name']);
579
		}
580
		$filter = $this->composeLdapFilter(self::LFILTER_GROUP_LIST);
581
582
		$this->applyFind('ldap_group_filter', $filter);
583
		return $this->result;
584
	}
585
586
	/**
587
	 * @return WizardResult|false
588
	 * @throws \Exception
589
	 */
590
	public function getUserListFilter() {
591
		$reqs = ['ldapHost', 'ldapBase'];
592
		if (!$this->configuration->usesLdapi()) {
593
			$reqs[] = 'ldapPort';
594
		}
595
		if (!$this->checkRequirements($reqs)) {
596
			return false;
597
		}
598
		//make sure the use display name is set
599
		$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...
600
		if ($displayName === '') {
601
			$d = $this->configuration->getDefaults();
602
			$this->applyFind('ldap_display_name', $d['ldap_display_name']);
603
		}
604
		$filter = $this->composeLdapFilter(self::LFILTER_USER_LIST);
605
		if (!$filter) {
606
			throw new \Exception('Cannot create filter');
607
		}
608
609
		$this->applyFind('ldap_userlist_filter', $filter);
610
		return $this->result;
611
	}
612
613
	/**
614
	 * @return WizardResult|false
615
	 * @throws \Exception
616
	 */
617
	public function getUserLoginFilter() {
618
		$reqs = ['ldapHost', 'ldapBase', 'ldapUserFilter'];
619
		if (!$this->configuration->usesLdapi()) {
620
			$reqs[] = 'ldapPort';
621
		}
622
		if (!$this->checkRequirements($reqs)) {
623
			return false;
624
		}
625
626
		$filter = $this->composeLdapFilter(self::LFILTER_LOGIN);
627
		if (!$filter) {
628
			throw new \Exception('Cannot create filter');
629
		}
630
631
		$this->applyFind('ldap_login_filter', $filter);
632
		return $this->result;
633
	}
634
635
	/**
636
	 * @return WizardResult|false
637
	 * @throws \Exception
638
	 */
639
  public function testLoginName(string $loginName) {
640
		$reqs = ['ldapHost', 'ldapBase', 'ldapUserFilter'];
641
		if (!$this->configuration->usesLdapi()) {
642
			$reqs[] = 'ldapPort';
643
		}
644
		if (!$this->checkRequirements($reqs)) {
645
			return false;
646
		}
647
648
		$cr = $this->access->connection->getConnectionResource();
649
		if (!$this->ldap->isResource($cr)) {
650
			throw new \Exception('connection error');
651
		}
652
		/** @var resource|\LDAP\Connection $cr */
653
654
		if (mb_strpos($this->access->connection->ldapLoginFilter, '%uid', 0, 'UTF-8')
655
			=== false) {
656
			throw new \Exception('missing placeholder');
657
		}
658
659
		$users = $this->access->countUsersByLoginName($loginName);
660
		if ($this->ldap->errno($cr) !== 0) {
661
			throw new \Exception($this->ldap->error($cr));
662
		}
663
		$filter = str_replace('%uid', $loginName, $this->access->connection->ldapLoginFilter);
664
		$this->result->addChange('ldap_test_loginname', $users);
665
		$this->result->addChange('ldap_test_effective_filter', $filter);
666
		return $this->result;
667
	}
668
669
	/**
670
	 * Tries to determine the port, requires given Host, User DN and Password
671
	 * @return WizardResult|false WizardResult on success, false otherwise
672
	 * @throws \Exception
673
	 */
674
	public function guessPortAndTLS() {
675
		if (!$this->checkRequirements(['ldapHost',
676
		])) {
677
			return false;
678
		}
679
		$this->checkHost();
680
		$portSettings = $this->getPortSettingsToTry();
681
682
		//proceed from the best configuration and return on first success
683
		foreach ($portSettings as $setting) {
684
			$p = $setting['port'];
685
			$t = $setting['tls'];
686
			$this->logger->debug(
687
				'Wiz: trying port '. $p . ', TLS '. $t,
688
				['app' => 'user_ldap']
689
			);
690
			//connectAndBind may throw Exception, it needs to be caught by the
691
			//callee of this method
692
693
			try {
694
				$settingsFound = $this->connectAndBind($p, $t);
695
			} catch (\Exception $e) {
696
				// any reply other than -1 (= cannot connect) is already okay,
697
				// because then we found the server
698
				// unavailable startTLS returns -11
699
				if ($e->getCode() > 0) {
700
					$settingsFound = true;
701
				} else {
702
					throw $e;
703
				}
704
			}
705
706
			if ($settingsFound === true) {
707
				$config = [
708
					'ldapPort' => $p,
709
					'ldapTLS' => (int)$t
710
				];
711
				$this->configuration->setConfiguration($config);
712
				$this->logger->debug(
713
					'Wiz: detected Port ' . $p,
714
					['app' => 'user_ldap']
715
				);
716
				$this->result->addChange('ldap_port', $p);
717
				return $this->result;
718
			}
719
		}
720
721
		//custom port, undetected (we do not brute force)
722
		return false;
723
	}
724
725
	/**
726
	 * tries to determine a base dn from User DN or LDAP Host
727
	 * @return WizardResult|false WizardResult on success, false otherwise
728
	 */
729
	public function guessBaseDN() {
730
		$reqs = ['ldapHost'];
731
		if (!$this->configuration->usesLdapi()) {
732
			$reqs[] = 'ldapPort';
733
		}
734
		if (!$this->checkRequirements($reqs)) {
735
			return false;
736
		}
737
738
		//check whether a DN is given in the agent name (99.9% of all cases)
739
		$base = null;
740
		$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...
741
		if ($i !== false) {
742
			$base = substr($this->configuration->ldapAgentName, $i);
743
			if ($this->testBaseDN($base)) {
744
				$this->applyFind('ldap_base', $base);
745
				return $this->result;
746
			}
747
		}
748
749
		//this did not help :(
750
		//Let's see whether we can parse the Host URL and convert the domain to
751
		//a base DN
752
		$helper = \OC::$server->get(Helper::class);
753
		$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...
754
		if (!$domain) {
755
			return false;
756
		}
757
758
		$dparts = explode('.', $domain);
759
		while (count($dparts) > 0) {
760
			$base2 = 'dc=' . implode(',dc=', $dparts);
761
			if ($base !== $base2 && $this->testBaseDN($base2)) {
762
				$this->applyFind('ldap_base', $base2);
763
				return $this->result;
764
			}
765
			array_shift($dparts);
766
		}
767
768
		return false;
769
	}
770
771
	/**
772
	 * sets the found value for the configuration key in the WizardResult
773
	 * as well as in the Configuration instance
774
	 * @param string $key the configuration key
775
	 * @param string $value the (detected) value
776
	 *
777
	 */
778
	private function applyFind(string $key, string $value): void {
779
		$this->result->addChange($key, $value);
780
		$this->configuration->setConfiguration([$key => $value]);
781
	}
782
783
	/**
784
	 * Checks, whether a port was entered in the Host configuration
785
	 * field. In this case the port will be stripped off, but also stored as
786
	 * setting.
787
	 */
788
	private function checkHost(): void {
789
		$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...
790
		$hostInfo = parse_url($host);
791
792
		//removes Port from Host
793
		if (is_array($hostInfo) && isset($hostInfo['port'])) {
794
			$port = $hostInfo['port'];
795
			$host = str_replace(':'.$port, '', $host);
796
			$this->applyFind('ldap_host', $host);
797
			$this->applyFind('ldap_port', (string)$port);
798
		}
799
	}
800
801
	/**
802
	 * tries to detect the group member association attribute which is
803
	 * one of 'uniqueMember', 'memberUid', 'member', 'gidNumber'
804
	 * @return string|false string with the attribute name, false on error
805
	 * @throws \Exception
806
	 */
807
	private function detectGroupMemberAssoc() {
808
		$possibleAttrs = ['uniqueMember', 'memberUid', 'member', 'gidNumber', 'zimbraMailForwardingAddress'];
809
		$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...
810
		if (empty($filter)) {
811
			return false;
812
		}
813
		$cr = $this->getConnection();
814
		if (!$cr) {
815
			throw new \Exception('Could not connect to LDAP');
816
		}
817
		$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...
818
		$rr = $this->ldap->search($cr, $base, $filter, $possibleAttrs, 0, 1000);
819
		if (!$this->ldap->isResource($rr)) {
820
			return false;
821
		}
822
		/** @var resource|\LDAP\Result $rr */
823
		$er = $this->ldap->firstEntry($cr, $rr);
824
		while ($this->ldap->isResource($er)) {
825
			$this->ldap->getDN($cr, $er);
826
			$attrs = $this->ldap->getAttributes($cr, $er);
827
			$result = [];
828
			$possibleAttrsCount = count($possibleAttrs);
829
			for ($i = 0; $i < $possibleAttrsCount; $i++) {
830
				if (isset($attrs[$possibleAttrs[$i]])) {
831
					$result[$possibleAttrs[$i]] = $attrs[$possibleAttrs[$i]]['count'];
832
				}
833
			}
834
			if (!empty($result)) {
835
				natsort($result);
836
				return key($result);
837
			}
838
839
			$er = $this->ldap->nextEntry($cr, $er);
840
		}
841
842
		return false;
843
	}
844
845
	/**
846
	 * Checks whether for a given BaseDN results will be returned
847
	 * @param string $base the BaseDN to test
848
	 * @return bool true on success, false otherwise
849
	 * @throws \Exception
850
	 */
851
	private function testBaseDN(string $base): bool {
852
		$cr = $this->getConnection();
853
		if (!$cr) {
854
			throw new \Exception('Could not connect to LDAP');
855
		}
856
857
		//base is there, let's validate it. If we search for anything, we should
858
		//get a result set > 0 on a proper base
859
		$rr = $this->ldap->search($cr, $base, 'objectClass=*', ['dn'], 0, 1);
860
		if (!$this->ldap->isResource($rr)) {
861
			$errorNo = $this->ldap->errno($cr);
862
			$errorMsg = $this->ldap->error($cr);
863
			$this->logger->info(
864
				'Wiz: Could not search base '.$base.' Error '.$errorNo.': '.$errorMsg,
865
				['app' => 'user_ldap']
866
			);
867
			return false;
868
		}
869
		/** @var resource|\LDAP\Result $rr */
870
		$entries = $this->ldap->countEntries($cr, $rr);
871
		return ($entries !== false) && ($entries > 0);
872
	}
873
874
	/**
875
	 * Checks whether the server supports memberOf in LDAP Filter.
876
	 * Note: at least in OpenLDAP, availability of memberOf is dependent on
877
	 * a configured objectClass. I.e. not necessarily for all available groups
878
	 * memberOf does work.
879
	 *
880
	 * @return bool true if it does, false otherwise
881
	 * @throws \Exception
882
	 */
883
	private function testMemberOf(): bool {
884
		$cr = $this->getConnection();
885
		if (!$cr) {
886
			throw new \Exception('Could not connect to LDAP');
887
		}
888
		$result = $this->access->countUsers('memberOf=*', ['memberOf'], 1);
889
		if (is_int($result) && $result > 0) {
890
			return true;
891
		}
892
		return false;
893
	}
894
895
	/**
896
	 * creates an LDAP Filter from given configuration
897
	 * @param int $filterType int, for which use case the filter shall be created
898
	 * can be any of self::LFILTER_USER_LIST, self::LFILTER_LOGIN or
899
	 * self::LFILTER_GROUP_LIST
900
	 * @throws \Exception
901
	 */
902
	private function composeLdapFilter(int $filterType): string {
903
		$filter = '';
904
		$parts = 0;
905
		switch ($filterType) {
906
			case self::LFILTER_USER_LIST:
907
				$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...
908
				//glue objectclasses
909
				if (is_array($objcs) && count($objcs) > 0) {
910
					$filter .= '(|';
911
					foreach ($objcs as $objc) {
912
						$filter .= '(objectclass=' . $objc . ')';
913
					}
914
					$filter .= ')';
915
					$parts++;
916
				}
917
				//glue group memberships
918
				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...
919
					$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...
920
					if (is_array($cns) && count($cns) > 0) {
921
						$filter .= '(|';
922
						$cr = $this->getConnection();
923
						if (!$cr) {
924
							throw new \Exception('Could not connect to LDAP');
925
						}
926
						$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...
927
						foreach ($cns as $cn) {
928
							$rr = $this->ldap->search($cr, $base, 'cn=' . $cn, ['dn', 'primaryGroupToken']);
929
							if (!$this->ldap->isResource($rr)) {
930
								continue;
931
							}
932
							/** @var resource|\LDAP\Result $rr */
933
							$er = $this->ldap->firstEntry($cr, $rr);
934
							$attrs = $this->ldap->getAttributes($cr, $er);
935
							$dn = $this->ldap->getDN($cr, $er);
936
							if ($dn === false || $dn === '') {
937
								continue;
938
							}
939
							$filterPart = '(memberof=' . $dn . ')';
940
							if (isset($attrs['primaryGroupToken'])) {
941
								$pgt = $attrs['primaryGroupToken'][0];
942
								$primaryFilterPart = '(primaryGroupID=' . $pgt .')';
943
								$filterPart = '(|' . $filterPart . $primaryFilterPart . ')';
944
							}
945
							$filter .= $filterPart;
946
						}
947
						$filter .= ')';
948
					}
949
					$parts++;
950
				}
951
				//wrap parts in AND condition
952
				if ($parts > 1) {
953
					$filter = '(&' . $filter . ')';
954
				}
955
				if ($filter === '') {
956
					$filter = '(objectclass=*)';
957
				}
958
				break;
959
960
			case self::LFILTER_GROUP_LIST:
961
				$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...
962
				//glue objectclasses
963
				if (is_array($objcs) && count($objcs) > 0) {
964
					$filter .= '(|';
965
					foreach ($objcs as $objc) {
966
						$filter .= '(objectclass=' . $objc . ')';
967
					}
968
					$filter .= ')';
969
					$parts++;
970
				}
971
				//glue group memberships
972
				$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...
973
				if (is_array($cns) && count($cns) > 0) {
974
					$filter .= '(|';
975
					foreach ($cns as $cn) {
976
						$filter .= '(cn=' . $cn . ')';
977
					}
978
					$filter .= ')';
979
				}
980
				$parts++;
981
				//wrap parts in AND condition
982
				if ($parts > 1) {
983
					$filter = '(&' . $filter . ')';
984
				}
985
				break;
986
987
			case self::LFILTER_LOGIN:
988
				$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...
989
				$loginpart = '=%uid';
990
				$filterUsername = '';
991
				$userAttributes = $this->getUserAttributes();
992
				if ($userAttributes === false) {
993
					throw new \Exception('Failed to get user attributes');
994
				}
995
				$userAttributes = array_change_key_case(array_flip($userAttributes));
996
				$parts = 0;
997
998
				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...
999
					$attr = '';
1000
					if (isset($userAttributes['uid'])) {
1001
						$attr = 'uid';
1002
					} elseif (isset($userAttributes['samaccountname'])) {
1003
						$attr = 'samaccountname';
1004
					} elseif (isset($userAttributes['cn'])) {
1005
						//fallback
1006
						$attr = 'cn';
1007
					}
1008
					if ($attr !== '') {
1009
						$filterUsername = '(' . $attr . $loginpart . ')';
1010
						$parts++;
1011
					}
1012
				}
1013
1014
				$filterEmail = '';
1015
				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...
1016
					$filterEmail = '(|(mailPrimaryAddress=%uid)(mail=%uid))';
1017
					$parts++;
1018
				}
1019
1020
				$filterAttributes = '';
1021
				$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...
1022
				if (is_array($attrsToFilter) && count($attrsToFilter) > 0) {
1023
					$filterAttributes = '(|';
1024
					foreach ($attrsToFilter as $attribute) {
1025
						$filterAttributes .= '(' . $attribute . $loginpart . ')';
1026
					}
1027
					$filterAttributes .= ')';
1028
					$parts++;
1029
				}
1030
1031
				$filterLogin = '';
1032
				if ($parts > 1) {
1033
					$filterLogin = '(|';
1034
				}
1035
				$filterLogin .= $filterUsername;
1036
				$filterLogin .= $filterEmail;
1037
				$filterLogin .= $filterAttributes;
1038
				if ($parts > 1) {
1039
					$filterLogin .= ')';
1040
				}
1041
1042
				$filter = '(&'.$ulf.$filterLogin.')';
1043
				break;
1044
		}
1045
1046
		$this->logger->debug(
1047
			'Wiz: Final filter '.$filter,
1048
			['app' => 'user_ldap']
1049
		);
1050
1051
		return $filter;
1052
	}
1053
1054
	/**
1055
	 * Connects and Binds to an LDAP Server
1056
	 *
1057
	 * @param int $port the port to connect with
1058
	 * @param bool $tls whether startTLS is to be used
1059
	 * @throws \Exception
1060
	 */
1061
	private function connectAndBind(int $port, bool $tls): bool {
1062
		//connect, does not really trigger any server communication
1063
		$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...
1064
		$hostInfo = parse_url((string)$host);
1065
		if (!is_string($host) || !$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...
1066
			throw new \Exception(self::$l->t('Invalid Host'));
1067
		}
1068
		$this->logger->debug(
1069
			'Wiz: Attempting to connect',
1070
			['app' => 'user_ldap']
1071
		);
1072
		$cr = $this->ldap->connect($host, (string)$port);
1073
		if (!$this->ldap->isResource($cr)) {
1074
			throw new \Exception(self::$l->t('Invalid Host'));
1075
		}
1076
		/** @var resource|\LDAP\Connection $cr */
1077
1078
		//set LDAP options
1079
		$this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
1080
		$this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
1081
		$this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
1082
1083
		try {
1084
			if ($tls) {
1085
				$isTlsWorking = @$this->ldap->startTls($cr);
1086
				if (!$isTlsWorking) {
1087
					return false;
1088
				}
1089
			}
1090
1091
			$this->logger->debug(
1092
				'Wiz: Attempting to Bind',
1093
				['app' => 'user_ldap']
1094
			);
1095
			//interesting part: do the bind!
1096
			$login = $this->ldap->bind($cr,
1097
				$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...
1098
				$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...
1099
			);
1100
			$errNo = $this->ldap->errno($cr);
1101
			$error = $this->ldap->error($cr);
1102
			$this->ldap->unbind($cr);
1103
		} catch (ServerNotAvailableException $e) {
1104
			return false;
1105
		}
1106
1107
		if ($login === true) {
1108
			$this->logger->debug(
1109
				'Wiz: Bind successful to Port '. $port . ' TLS ' . (int)$tls,
1110
				['app' => 'user_ldap']
1111
			);
1112
			return true;
1113
		}
1114
1115
		if ($errNo === -1) {
1116
			//host, port or TLS wrong
1117
			return false;
1118
		}
1119
		throw new \Exception($error, $errNo);
1120
	}
1121
1122
	/**
1123
	 * checks whether a valid combination of agent and password has been
1124
	 * provided (either two values or nothing for anonymous connect)
1125
	 * @return bool true if everything is fine, false otherwise
1126
	 */
1127
	private function checkAgentRequirements(): bool {
1128
		$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...
1129
		$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...
1130
1131
		return
1132
			($agent !== '' && $pwd !== '')
1133
			|| ($agent === '' && $pwd === '')
1134
		;
1135
	}
1136
1137
	private function checkRequirements(array $reqs): bool {
1138
		$this->checkAgentRequirements();
1139
		foreach ($reqs as $option) {
1140
			$value = $this->configuration->$option;
1141
			if (empty($value)) {
1142
				return false;
1143
			}
1144
		}
1145
		return true;
1146
	}
1147
1148
	/**
1149
	 * does a cumulativeSearch on LDAP to get different values of a
1150
	 * specified attribute
1151
	 * @param string[] $filters array, the filters that shall be used in the search
1152
	 * @param string $attr the attribute of which a list of values shall be returned
1153
	 * @param int $dnReadLimit the amount of how many DNs should be analyzed.
1154
	 * The lower, the faster
1155
	 * @param string $maxF string. if not null, this variable will have the filter that
1156
	 * yields most result entries
1157
	 * @return array|false an array with the values on success, false otherwise
1158
	 */
1159
	public function cumulativeSearchOnAttribute(array $filters, string $attr, int $dnReadLimit = 3, ?string &$maxF = null) {
1160
		$dnRead = [];
1161
		$foundItems = [];
1162
		$maxEntries = 0;
1163
		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...
1164
		   || !isset($this->configuration->ldapBase[0])) {
1165
			return false;
1166
		}
1167
		$base = $this->configuration->ldapBase[0];
1168
		$cr = $this->getConnection();
1169
		if (!$this->ldap->isResource($cr)) {
1170
			return false;
1171
		}
1172
		/** @var resource|\LDAP\Connection $cr */
1173
		$lastFilter = null;
1174
		if (isset($filters[count($filters) - 1])) {
1175
			$lastFilter = $filters[count($filters) - 1];
1176
		}
1177
		foreach ($filters as $filter) {
1178
			if ($lastFilter === $filter && count($foundItems) > 0) {
1179
				//skip when the filter is a wildcard and results were found
1180
				continue;
1181
			}
1182
			// 20k limit for performance and reason
1183
			$rr = $this->ldap->search($cr, $base, $filter, [$attr], 0, 20000);
1184
			if (!$this->ldap->isResource($rr)) {
1185
				continue;
1186
			}
1187
			/** @var resource|\LDAP\Result $rr */
1188
			$entries = $this->ldap->countEntries($cr, $rr);
1189
			$getEntryFunc = 'firstEntry';
1190
			if (($entries !== false) && ($entries > 0)) {
1191
				if (!is_null($maxF) && $entries > $maxEntries) {
1192
					$maxEntries = $entries;
1193
					$maxF = $filter;
1194
				}
1195
				$dnReadCount = 0;
1196
				do {
1197
					$entry = $this->ldap->$getEntryFunc($cr, $rr);
1198
					$getEntryFunc = 'nextEntry';
1199
					if (!$this->ldap->isResource($entry)) {
1200
						continue 2;
1201
					}
1202
					$rr = $entry; //will be expected by nextEntry next round
1203
					$attributes = $this->ldap->getAttributes($cr, $entry);
1204
					$dn = $this->ldap->getDN($cr, $entry);
1205
					if ($attributes === false || $dn === false || in_array($dn, $dnRead)) {
1206
						continue;
1207
					}
1208
					$newItems = [];
1209
					$state = $this->getAttributeValuesFromEntry(
1210
						$attributes,
1211
						$attr,
1212
						$newItems
1213
					);
1214
					$dnReadCount++;
1215
					$foundItems = array_merge($foundItems, $newItems);
1216
					$dnRead[] = $dn;
1217
				} 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...
1218
						|| $this->ldap->isResource($entry))
1219
						&& ($dnReadLimit === 0 || $dnReadCount < $dnReadLimit));
1220
			}
1221
		}
1222
1223
		return array_unique($foundItems);
1224
	}
1225
1226
	/**
1227
	 * determines if and which $attr are available on the LDAP server
1228
	 * @param string[] $objectclasses the objectclasses to use as search filter
1229
	 * @param string $attr the attribute to look for
1230
	 * @param string $dbkey the dbkey of the setting the feature is connected to
1231
	 * @param string $confkey the confkey counterpart for the $dbkey as used in the
1232
	 * Configuration class
1233
	 * @param bool $po whether the objectClass with most result entries
1234
	 * shall be pre-selected via the result
1235
	 * @return array list of found items.
1236
	 * @throws \Exception
1237
	 */
1238
	private function determineFeature(array $objectclasses, string $attr, string $dbkey, string $confkey, bool $po = false): array {
1239
		$cr = $this->getConnection();
1240
		if (!$cr) {
1241
			throw new \Exception('Could not connect to LDAP');
1242
		}
1243
		$p = 'objectclass=';
1244
		foreach ($objectclasses as $key => $value) {
1245
			$objectclasses[$key] = $p.$value;
1246
		}
1247
		$maxEntryObjC = '';
1248
1249
		//how deep to dig?
1250
		//When looking for objectclasses, testing few entries is sufficient,
1251
		$dig = 3;
1252
1253
		$availableFeatures =
1254
			$this->cumulativeSearchOnAttribute($objectclasses, $attr,
1255
				$dig, $maxEntryObjC);
1256
		if (is_array($availableFeatures)
1257
		   && count($availableFeatures) > 0) {
1258
			natcasesort($availableFeatures);
1259
			//natcasesort keeps indices, but we must get rid of them for proper
1260
			//sorting in the web UI. Therefore: array_values
1261
			$this->result->addOptions($dbkey, array_values($availableFeatures));
1262
		} else {
1263
			throw new \Exception(self::$l->t('Could not find the desired feature'));
1264
		}
1265
1266
		$setFeatures = $this->configuration->$confkey;
1267
		if (is_array($setFeatures) && !empty($setFeatures)) {
1268
			//something is already configured? pre-select it.
1269
			$this->result->addChange($dbkey, $setFeatures);
1270
		} elseif ($po && $maxEntryObjC !== '') {
1271
			//pre-select objectclass with most result entries
1272
			$maxEntryObjC = str_replace($p, '', $maxEntryObjC);
1273
			$this->applyFind($dbkey, $maxEntryObjC);
1274
			$this->result->addChange($dbkey, $maxEntryObjC);
1275
		}
1276
1277
		return $availableFeatures;
1278
	}
1279
1280
	/**
1281
	 * appends a list of values fr
1282
	 * @param array $result the return value from ldap_get_attributes
1283
	 * @param string $attribute the attribute values to look for
1284
	 * @param array &$known new values will be appended here
1285
	 * @return int state on of the class constants LRESULT_PROCESSED_OK,
1286
	 * LRESULT_PROCESSED_INVALID or LRESULT_PROCESSED_SKIP
1287
	 */
1288
	private function getAttributeValuesFromEntry(array $result, string $attribute, array &$known): int {
1289
		if (!isset($result['count'])
1290
		   || !$result['count'] > 0) {
1291
			return self::LRESULT_PROCESSED_INVALID;
1292
		}
1293
1294
		// strtolower on all keys for proper comparison
1295
		$result = \OCP\Util::mb_array_change_key_case($result);
1296
		$attribute = strtolower($attribute);
1297
		if (isset($result[$attribute])) {
1298
			foreach ($result[$attribute] as $key => $val) {
1299
				if ($key === 'count') {
1300
					continue;
1301
				}
1302
				if (!in_array($val, $known)) {
1303
					$known[] = $val;
1304
				}
1305
			}
1306
			return self::LRESULT_PROCESSED_OK;
1307
		} else {
1308
			return self::LRESULT_PROCESSED_SKIP;
1309
		}
1310
	}
1311
1312
	/**
1313
	 * @return resource|\LDAP\Connection|false a link resource on success, otherwise false
1314
	 */
1315
	private function getConnection() {
1316
		if (!is_null($this->cr)) {
1317
			return $this->cr;
1318
		}
1319
1320
		$cr = $this->ldap->connect(
1321
			$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...
1322
			$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...
1323
		);
1324
1325
		if ($cr === false) {
1326
			return false;
1327
		}
1328
1329
		$this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
1330
		$this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
1331
		$this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
1332
		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...
1333
			$this->ldap->startTls($cr);
1334
		}
1335
1336
		$lo = @$this->ldap->bind($cr,
1337
			$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...
1338
			$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...
1339
		if ($lo === true) {
1340
			$this->cr = $cr;
1341
			return $cr;
1342
		}
1343
1344
		return false;
1345
	}
1346
1347
	private function getDefaultLdapPortSettings(): array {
1348
		static $settings = [
1349
			['port' => 7636, 'tls' => false],
1350
			['port' => 636, 'tls' => false],
1351
			['port' => 7389, 'tls' => true],
1352
			['port' => 389, 'tls' => true],
1353
			['port' => 7389, 'tls' => false],
1354
			['port' => 389, 'tls' => false],
1355
		];
1356
		return $settings;
1357
	}
1358
1359
	private function getPortSettingsToTry(): array {
1360
		//389 ← LDAP / Unencrypted or StartTLS
1361
		//636 ← LDAPS / SSL
1362
		//7xxx ← UCS. need to be checked first, because both ports may be open
1363
		$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...
1364
		$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...
1365
		$portSettings = [];
1366
1367
		//In case the port is already provided, we will check this first
1368
		if ($port > 0) {
1369
			$hostInfo = parse_url($host);
1370
			if (!(is_array($hostInfo)
1371
				&& isset($hostInfo['scheme'])
1372
				&& stripos($hostInfo['scheme'], 'ldaps') !== false)) {
1373
				$portSettings[] = ['port' => $port, 'tls' => true];
1374
			}
1375
			$portSettings[] = ['port' => $port, 'tls' => false];
1376
		} elseif ($this->configuration->usesLdapi()) {
1377
			$portSettings[] = ['port' => '', 'tls' => false];
1378
		}
1379
1380
		//default ports
1381
		$portSettings = array_merge($portSettings,
1382
			$this->getDefaultLdapPortSettings());
1383
1384
		return $portSettings;
1385
	}
1386
}
1387