Wizard.php 38 KB

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