1
0

Wizard.php 38 KB

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