You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Wizard.php 38KB

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