Wizard.php 39 KB

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