Wizard.php 38 KB

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