PageRenderTime 61ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/apps/user_ldap/lib/access.php

https://github.com/jlgg/simple_trash
PHP | 884 lines | 561 code | 86 blank | 237 comment | 102 complexity | c9acaa0a6a2d070a17024ad1b060f97d MD5 | raw file
Possible License(s): AGPL-3.0, AGPL-1.0, MPL-2.0-no-copyleft-exception
  1. <?php
  2. /**
  3. * ownCloud – LDAP Access
  4. *
  5. * @author Arthur Schiwon
  6. * @copyright 2012 Arthur Schiwon blizzz@owncloud.com
  7. *
  8. * This library is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  10. * License as published by the Free Software Foundation; either
  11. * version 3 of the License, or any later version.
  12. *
  13. * This library is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public
  19. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
  20. *
  21. */
  22. namespace OCA\user_ldap\lib;
  23. abstract class Access {
  24. protected $connection;
  25. //never ever check this var directly, always use getPagedSearchResultState
  26. protected $pagedSearchedSuccessful;
  27. public function setConnector(Connection &$connection) {
  28. $this->connection = $connection;
  29. }
  30. private function checkConnection() {
  31. return ($this->connection instanceof Connection);
  32. }
  33. /**
  34. * @brief reads a given attribute for an LDAP record identified by a DN
  35. * @param $dn the record in question
  36. * @param $attr the attribute that shall be retrieved
  37. * if empty, just check the record's existence
  38. * @returns an array of values on success or an empty
  39. * array if $attr is empty, false otherwise
  40. *
  41. * Reads an attribute from an LDAP entry or check if entry exists
  42. */
  43. public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
  44. if(!$this->checkConnection()) {
  45. \OCP\Util::writeLog('user_ldap', 'No LDAP Connector assigned, access impossible for readAttribute.', \OCP\Util::WARN);
  46. return false;
  47. }
  48. $cr = $this->connection->getConnectionResource();
  49. if(!is_resource($cr)) {
  50. //LDAP not available
  51. \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
  52. return false;
  53. }
  54. $rr = @ldap_read($cr, $dn, $filter, array($attr));
  55. $dn = $this->DNasBaseParameter($dn);
  56. if(!is_resource($rr)) {
  57. \OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN '.$dn, \OCP\Util::DEBUG);
  58. //in case an error occurs , e.g. object does not exist
  59. return false;
  60. }
  61. if (empty($attr)) {
  62. \OCP\Util::writeLog('user_ldap', 'readAttribute: '.$dn.' found', \OCP\Util::DEBUG);
  63. return array();
  64. }
  65. $er = ldap_first_entry($cr, $rr);
  66. if(!is_resource($er)) {
  67. //did not match the filter, return false
  68. return false;
  69. }
  70. //LDAP attributes are not case sensitive
  71. $result = \OCP\Util::mb_array_change_key_case(ldap_get_attributes($cr, $er), MB_CASE_LOWER, 'UTF-8');
  72. $attr = mb_strtolower($attr, 'UTF-8');
  73. if(isset($result[$attr]) && $result[$attr]['count'] > 0) {
  74. $values = array();
  75. for($i=0;$i<$result[$attr]['count'];$i++) {
  76. if($this->resemblesDN($attr)) {
  77. $values[] = $this->sanitizeDN($result[$attr][$i]);
  78. } elseif(strtolower($attr) == 'objectguid') {
  79. $values[] = $this->convertObjectGUID2Str($result[$attr][$i]);
  80. } else {
  81. $values[] = $result[$attr][$i];
  82. }
  83. }
  84. return $values;
  85. }
  86. \OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, \OCP\Util::DEBUG);
  87. return false;
  88. }
  89. /**
  90. * @brief checks wether the given attribute`s valua is probably a DN
  91. * @param $attr the attribute in question
  92. * @return if so true, otherwise false
  93. */
  94. private function resemblesDN($attr) {
  95. $resemblingAttributes = array(
  96. 'dn',
  97. 'uniquemember',
  98. 'member'
  99. );
  100. return in_array($attr, $resemblingAttributes);
  101. }
  102. /**
  103. * @brief sanitizes a DN received from the LDAP server
  104. * @param $dn the DN in question
  105. * @return the sanitized DN
  106. */
  107. private function sanitizeDN($dn) {
  108. //OID sometimes gives back DNs with whitespace after the comma a la "uid=foo, cn=bar, dn=..." We need to tackle this!
  109. $dn = preg_replace('/([^\\\]),(\s+)/u', '\1,', $dn);
  110. //make comparisons and everything work
  111. $dn = mb_strtolower($dn, 'UTF-8');
  112. //escape DN values according to RFC 2253 – this is already done by ldap_explode_dn
  113. //to use the DN in search filters, \ needs to be escaped to \5c additionally
  114. //to use them in bases, we convert them back to simple backslashes in readAttribute()
  115. $replacements = array(
  116. '\,' => '\5c2C',
  117. '\=' => '\5c3D',
  118. '\+' => '\5c2B',
  119. '\<' => '\5c3C',
  120. '\>' => '\5c3E',
  121. '\;' => '\5c3B',
  122. '\"' => '\5c22',
  123. '\#' => '\5c23',
  124. );
  125. $dn = str_replace(array_keys($replacements), array_values($replacements), $dn);
  126. return $dn;
  127. }
  128. /**
  129. * gives back the database table for the query
  130. */
  131. private function getMapTable($isUser) {
  132. if($isUser) {
  133. return '*PREFIX*ldap_user_mapping';
  134. } else {
  135. return '*PREFIX*ldap_group_mapping';
  136. }
  137. }
  138. /**
  139. * @brief returns the LDAP DN for the given internal ownCloud name of the group
  140. * @param $name the ownCloud name in question
  141. * @returns string with the LDAP DN on success, otherwise false
  142. *
  143. * returns the LDAP DN for the given internal ownCloud name of the group
  144. */
  145. public function groupname2dn($name) {
  146. $dn = $this->ocname2dn($name, false);
  147. if($dn) {
  148. return $dn;
  149. }
  150. return false;
  151. }
  152. /**
  153. * @brief returns the LDAP DN for the given internal ownCloud name of the user
  154. * @param $name the ownCloud name in question
  155. * @returns string with the LDAP DN on success, otherwise false
  156. *
  157. * returns the LDAP DN for the given internal ownCloud name of the user
  158. */
  159. public function username2dn($name) {
  160. $dn = $this->ocname2dn($name, true);
  161. if($dn) {
  162. return $dn;
  163. }
  164. return false;
  165. }
  166. /**
  167. * @brief returns the LDAP DN for the given internal ownCloud name
  168. * @param $name the ownCloud name in question
  169. * @param $isUser is it a user? otherwise group
  170. * @returns string with the LDAP DN on success, otherwise false
  171. *
  172. * returns the LDAP DN for the given internal ownCloud name
  173. */
  174. private function ocname2dn($name, $isUser) {
  175. $table = $this->getMapTable($isUser);
  176. $query = \OCP\DB::prepare('
  177. SELECT `ldap_dn`
  178. FROM `'.$table.'`
  179. WHERE `owncloud_name` = ?
  180. ');
  181. $record = $query->execute(array($name))->fetchOne();
  182. return $record;
  183. }
  184. /**
  185. * @brief returns the internal ownCloud name for the given LDAP DN of the group
  186. * @param $dn the dn of the group object
  187. * @param $ldapname optional, the display name of the object
  188. * @returns string with with the name to use in ownCloud, false on DN outside of search DN
  189. *
  190. * returns the internal ownCloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
  191. */
  192. public function dn2groupname($dn, $ldapname = null) {
  193. if(mb_strripos($dn, $this->sanitizeDN($this->connection->ldapBaseGroups), 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($this->sanitizeDN($this->connection->ldapBaseGroups), 'UTF-8'))) {
  194. return false;
  195. }
  196. return $this->dn2ocname($dn, $ldapname, false);
  197. }
  198. /**
  199. * @brief returns the internal ownCloud name for the given LDAP DN of the user
  200. * @param $dn the dn of the user object
  201. * @param $ldapname optional, the display name of the object
  202. * @returns string with with the name to use in ownCloud
  203. *
  204. * returns the internal ownCloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
  205. */
  206. public function dn2username($dn, $ldapname = null) {
  207. if(mb_strripos($dn, $this->sanitizeDN($this->connection->ldapBaseUsers), 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($this->sanitizeDN($this->connection->ldapBaseUsers), 'UTF-8'))) {
  208. return false;
  209. }
  210. return $this->dn2ocname($dn, $ldapname, true);
  211. }
  212. /**
  213. * @brief returns an internal ownCloud name for the given LDAP DN
  214. * @param $dn the dn of the user object
  215. * @param $ldapname optional, the display name of the object
  216. * @param $isUser optional, wether it is a user object (otherwise group assumed)
  217. * @returns string with with the name to use in ownCloud
  218. *
  219. * returns the internal ownCloud name for the given LDAP DN of the user, false on DN outside of search DN
  220. */
  221. public function dn2ocname($dn, $ldapname = null, $isUser = true) {
  222. $table = $this->getMapTable($isUser);
  223. if($isUser) {
  224. $fncFindMappedName = 'findMappedUser';
  225. $nameAttribute = $this->connection->ldapUserDisplayName;
  226. } else {
  227. $fncFindMappedName = 'findMappedGroup';
  228. $nameAttribute = $this->connection->ldapGroupDisplayName;
  229. }
  230. //let's try to retrieve the ownCloud name from the mappings table
  231. $ocname = $this->$fncFindMappedName($dn);
  232. if($ocname) {
  233. return $ocname;
  234. }
  235. //second try: get the UUID and check if it is known. Then, update the DN and return the name.
  236. $uuid = $this->getUUID($dn);
  237. if($uuid) {
  238. $query = \OCP\DB::prepare('
  239. SELECT `owncloud_name`
  240. FROM `'.$table.'`
  241. WHERE `directory_uuid` = ?
  242. ');
  243. $component = $query->execute(array($uuid))->fetchOne();
  244. if($component) {
  245. $query = \OCP\DB::prepare('
  246. UPDATE `'.$table.'`
  247. SET `ldap_dn` = ?
  248. WHERE `directory_uuid` = ?
  249. ');
  250. $query->execute(array($dn, $uuid));
  251. return $component;
  252. }
  253. }
  254. if(is_null($ldapname)) {
  255. $ldapname = $this->readAttribute($dn, $nameAttribute);
  256. if(!isset($ldapname[0]) && empty($ldapname[0])) {
  257. \OCP\Util::writeLog('user_ldap', 'No or empty name for '.$dn.'.', \OCP\Util::INFO);
  258. return false;
  259. }
  260. $ldapname = $ldapname[0];
  261. }
  262. $ldapname = $this->sanitizeUsername($ldapname);
  263. //a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
  264. if(($isUser && !\OCP\User::userExists($ldapname, 'OCA\\user_ldap\\USER_LDAP')) || (!$isUser && !\OC_Group::groupExists($ldapname))) {
  265. if($this->mapComponent($dn, $ldapname, $isUser)) {
  266. return $ldapname;
  267. }
  268. }
  269. //doh! There is a conflict. We need to distinguish between users/groups. Adding indexes is an idea, but not much of a help for the user. The DN is ugly, but for now the only reasonable way. But we transform it to a readable format and remove the first part to only give the path where this object is located.
  270. $oc_name = $this->alternateOwnCloudName($ldapname, $dn);
  271. if(($isUser && !\OCP\User::userExists($oc_name)) || (!$isUser && !\OC_Group::groupExists($oc_name))) {
  272. if($this->mapComponent($dn, $oc_name, $isUser)) {
  273. return $oc_name;
  274. }
  275. }
  276. //if everything else did not help..
  277. \OCP\Util::writeLog('user_ldap', 'Could not create unique ownCloud name for '.$dn.'.', \OCP\Util::INFO);
  278. return false;
  279. }
  280. /**
  281. * @brief gives back the user names as they are used ownClod internally
  282. * @param $ldapGroups an array with the ldap Users result in style of array ( array ('dn' => foo, 'uid' => bar), ... )
  283. * @returns an array with the user names to use in ownCloud
  284. *
  285. * gives back the user names as they are used ownClod internally
  286. */
  287. public function ownCloudUserNames($ldapUsers) {
  288. return $this->ldap2ownCloudNames($ldapUsers, true);
  289. }
  290. /**
  291. * @brief gives back the group names as they are used ownClod internally
  292. * @param $ldapGroups an array with the ldap Groups result in style of array ( array ('dn' => foo, 'cn' => bar), ... )
  293. * @returns an array with the group names to use in ownCloud
  294. *
  295. * gives back the group names as they are used ownClod internally
  296. */
  297. public function ownCloudGroupNames($ldapGroups) {
  298. return $this->ldap2ownCloudNames($ldapGroups, false);
  299. }
  300. private function findMappedUser($dn) {
  301. static $query = null;
  302. if(is_null($query)) {
  303. $query = \OCP\DB::prepare('
  304. SELECT `owncloud_name`
  305. FROM `'.$this->getMapTable(true).'`
  306. WHERE `ldap_dn` = ?'
  307. );
  308. }
  309. $res = $query->execute(array($dn))->fetchOne();
  310. if($res) {
  311. return $res;
  312. }
  313. return false;
  314. }
  315. private function findMappedGroup($dn) {
  316. static $query = null;
  317. if(is_null($query)) {
  318. $query = \OCP\DB::prepare('
  319. SELECT `owncloud_name`
  320. FROM `'.$this->getMapTable(false).'`
  321. WHERE `ldap_dn` = ?'
  322. );
  323. }
  324. $res = $query->execute(array($dn))->fetchOne();
  325. if($res) {
  326. return $res;
  327. }
  328. return false;
  329. }
  330. private function ldap2ownCloudNames($ldapObjects, $isUsers) {
  331. if($isUsers) {
  332. $nameAttribute = $this->connection->ldapUserDisplayName;
  333. } else {
  334. $nameAttribute = $this->connection->ldapGroupDisplayName;
  335. }
  336. $ownCloudNames = array();
  337. foreach($ldapObjects as $ldapObject) {
  338. $nameByLDAP = isset($ldapObject[$nameAttribute]) ? $ldapObject[$nameAttribute] : null;
  339. $ocname = $this->dn2ocname($ldapObject['dn'], $nameByLDAP, $isUsers);
  340. if($ocname) {
  341. $ownCloudNames[] = $ocname;
  342. }
  343. continue;
  344. }
  345. return $ownCloudNames;
  346. }
  347. /**
  348. * @brief creates a hopefully unique name for owncloud based on the display name and the dn of the LDAP object
  349. * @param $name the display name of the object
  350. * @param $dn the dn of the object
  351. * @returns string with with the name to use in ownCloud
  352. *
  353. * creates a hopefully unique name for owncloud based on the display name and the dn of the LDAP object
  354. */
  355. private function alternateOwnCloudName($name, $dn) {
  356. $ufn = ldap_dn2ufn($dn);
  357. $name = $name . '@' . trim(\OCP\Util::mb_substr_replace($ufn, '', 0, mb_strpos($ufn, ',', 0, 'UTF-8'), 'UTF-8'));
  358. $name = $this->sanitizeUsername($name);
  359. return $name;
  360. }
  361. /**
  362. * @brief retrieves all known groups from the mappings table
  363. * @returns array with the results
  364. *
  365. * retrieves all known groups from the mappings table
  366. */
  367. private function mappedGroups() {
  368. return $this->mappedComponents(false);
  369. }
  370. /**
  371. * @brief retrieves all known users from the mappings table
  372. * @returns array with the results
  373. *
  374. * retrieves all known users from the mappings table
  375. */
  376. private function mappedUsers() {
  377. return $this->mappedComponents(true);
  378. }
  379. private function mappedComponents($isUsers) {
  380. $table = $this->getMapTable($isUsers);
  381. $query = \OCP\DB::prepare('
  382. SELECT `ldap_dn`, `owncloud_name`
  383. FROM `'. $table . '`'
  384. );
  385. return $query->execute()->fetchAll();
  386. }
  387. /**
  388. * @brief inserts a new user or group into the mappings table
  389. * @param $dn the record in question
  390. * @param $ocname the name to use in ownCloud
  391. * @param $isUser is it a user or a group?
  392. * @returns true on success, false otherwise
  393. *
  394. * inserts a new user or group into the mappings table
  395. */
  396. private function mapComponent($dn, $ocname, $isUser = true) {
  397. $table = $this->getMapTable($isUser);
  398. $sqlAdjustment = '';
  399. $dbtype = \OCP\Config::getSystemValue('dbtype');
  400. if($dbtype == 'mysql') {
  401. $sqlAdjustment = 'FROM DUAL';
  402. }
  403. $insert = \OCP\DB::prepare('
  404. INSERT INTO `'.$table.'` (`ldap_dn`, `owncloud_name`, `directory_uuid`)
  405. SELECT ?,?,?
  406. '.$sqlAdjustment.'
  407. WHERE NOT EXISTS (
  408. SELECT 1
  409. FROM `'.$table.'`
  410. WHERE `ldap_dn` = ?
  411. OR `owncloud_name` = ?)
  412. ');
  413. //feed the DB
  414. $res = $insert->execute(array($dn, $ocname, $this->getUUID($dn), $dn, $ocname));
  415. if(\OCP\DB::isError($res)) {
  416. return false;
  417. }
  418. $insRows = $res->numRows();
  419. if($insRows == 0) {
  420. return false;
  421. }
  422. return true;
  423. }
  424. public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null) {
  425. return $this->fetchList($this->searchUsers($filter, $attr, $limit, $offset), (count($attr) > 1));
  426. }
  427. public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
  428. return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), (count($attr) > 1));
  429. }
  430. private function fetchList($list, $manyAttributes) {
  431. if(is_array($list)) {
  432. if($manyAttributes) {
  433. return $list;
  434. } else {
  435. return array_unique($list, SORT_LOCALE_STRING);
  436. }
  437. }
  438. //error cause actually, maybe throw an exception in future.
  439. return array();
  440. }
  441. /**
  442. * @brief executes an LDAP search, optimized for Users
  443. * @param $filter the LDAP filter for the search
  444. * @param $attr optional, when a certain attribute shall be filtered out
  445. * @returns array with the search result
  446. *
  447. * Executes an LDAP search
  448. */
  449. public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
  450. return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
  451. }
  452. /**
  453. * @brief executes an LDAP search, optimized for Groups
  454. * @param $filter the LDAP filter for the search
  455. * @param $attr optional, when a certain attribute shall be filtered out
  456. * @returns array with the search result
  457. *
  458. * Executes an LDAP search
  459. */
  460. public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
  461. return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
  462. }
  463. /**
  464. * @brief executes an LDAP search
  465. * @param $filter the LDAP filter for the search
  466. * @param $base the LDAP subtree that shall be searched
  467. * @param $attr optional, when a certain attribute shall be filtered out
  468. * @returns array with the search result
  469. *
  470. * Executes an LDAP search
  471. */
  472. private function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
  473. if(!is_null($attr) && !is_array($attr)) {
  474. $attr = array(mb_strtolower($attr, 'UTF-8'));
  475. }
  476. // See if we have a resource, in case not cancel with message
  477. $link_resource = $this->connection->getConnectionResource();
  478. if(!is_resource($link_resource)) {
  479. // Seems like we didn't find any resource.
  480. // Return an empty array just like before.
  481. \OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', \OCP\Util::DEBUG);
  482. return array();
  483. }
  484. //check wether paged search should be attempted
  485. $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, $limit, $offset);
  486. $sr = ldap_search($link_resource, $base, $filter, $attr);
  487. if(!$sr) {
  488. \OCP\Util::writeLog('user_ldap', 'Error when searching: '.ldap_error($link_resource).' code '.ldap_errno($link_resource), \OCP\Util::ERROR);
  489. \OCP\Util::writeLog('user_ldap', 'Attempt for Paging? '.print_r($pagedSearchOK, true), \OCP\Util::ERROR);
  490. return array();
  491. }
  492. $findings = ldap_get_entries($link_resource, $sr );
  493. if($pagedSearchOK) {
  494. \OCP\Util::writeLog('user_ldap', 'Paged search successful', \OCP\Util::INFO);
  495. ldap_control_paged_result_response($link_resource, $sr, $cookie);
  496. \OCP\Util::writeLog('user_ldap', 'Set paged search cookie '.$cookie, \OCP\Util::INFO);
  497. $this->setPagedResultCookie($filter, $limit, $offset, $cookie);
  498. //browsing through prior pages to get the cookie for the new one
  499. if($skipHandling) {
  500. return;
  501. }
  502. //if count is bigger, then the server does not support paged search. Instead, he did a normal search. We set a flag here, so the callee knows how to deal with it.
  503. if($findings['count'] <= $limit) {
  504. $this->pagedSearchedSuccessful = true;
  505. }
  506. } else {
  507. \OCP\Util::writeLog('user_ldap', 'Paged search failed :(', \OCP\Util::INFO);
  508. }
  509. // if we're here, probably no connection resource is returned.
  510. // to make ownCloud behave nicely, we simply give back an empty array.
  511. if(is_null($findings)) {
  512. return array();
  513. }
  514. if(!is_null($attr)) {
  515. $selection = array();
  516. $multiarray = false;
  517. if(count($attr) > 1) {
  518. $multiarray = true;
  519. $i = 0;
  520. }
  521. foreach($findings as $item) {
  522. if(!is_array($item)) {
  523. continue;
  524. }
  525. $item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
  526. if($multiarray) {
  527. foreach($attr as $key) {
  528. $key = mb_strtolower($key, 'UTF-8');
  529. if(isset($item[$key])) {
  530. if($key != 'dn') {
  531. $selection[$i][$key] = $this->resemblesDN($key) ? $this->sanitizeDN($item[$key][0]) : $item[$key][0];
  532. } else {
  533. $selection[$i][$key] = $this->sanitizeDN($item[$key]);
  534. }
  535. }
  536. }
  537. $i++;
  538. } else {
  539. //tribute to case insensitivity
  540. $key = mb_strtolower($attr[0], 'UTF-8');
  541. if(isset($item[$key])) {
  542. if($this->resemblesDN($key)) {
  543. $selection[] = $this->sanitizeDN($item[$key]);
  544. } else {
  545. $selection[] = $item[$key];
  546. }
  547. }
  548. }
  549. }
  550. $findings = $selection;
  551. }
  552. //we slice the findings, when
  553. //a) paged search insuccessful, though attempted
  554. //b) no paged search, but limit set
  555. if((!$this->pagedSearchedSuccessful
  556. && $pagedSearchOK)
  557. || (
  558. !$pagedSearchOK
  559. && !is_null($limit)
  560. )
  561. ) {
  562. $findings = array_slice($findings, intval($offset), $limit);
  563. }
  564. return $findings;
  565. }
  566. public function sanitizeUsername($name) {
  567. if($this->connection->ldapIgnoreNamingRules) {
  568. return $name;
  569. }
  570. // Translitaration
  571. //latin characters to ASCII
  572. $name = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
  573. //REPLACEMENTS
  574. $name = \OCP\Util::mb_str_replace(' ', '_', $name, 'UTF-8');
  575. //every remaining unallowed characters will be removed
  576. $name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
  577. return $name;
  578. }
  579. /**
  580. * @brief combines the input filters with AND
  581. * @param $filters array, the filters to connect
  582. * @returns the combined filter
  583. *
  584. * Combines Filter arguments with AND
  585. */
  586. public function combineFilterWithAnd($filters) {
  587. return $this->combineFilter($filters, '&');
  588. }
  589. /**
  590. * @brief combines the input filters with AND
  591. * @param $filters array, the filters to connect
  592. * @returns the combined filter
  593. *
  594. * Combines Filter arguments with AND
  595. */
  596. public function combineFilterWithOr($filters) {
  597. return $this->combineFilter($filters, '|');
  598. }
  599. /**
  600. * @brief combines the input filters with given operator
  601. * @param $filters array, the filters to connect
  602. * @param $operator either & or |
  603. * @returns the combined filter
  604. *
  605. * Combines Filter arguments with AND
  606. */
  607. private function combineFilter($filters, $operator) {
  608. $combinedFilter = '('.$operator;
  609. foreach($filters as $filter) {
  610. if($filter[0] != '(') {
  611. $filter = '('.$filter.')';
  612. }
  613. $combinedFilter.=$filter;
  614. }
  615. $combinedFilter.=')';
  616. return $combinedFilter;
  617. }
  618. public function areCredentialsValid($name, $password) {
  619. $name = $this->DNasBaseParameter($name);
  620. $testConnection = clone $this->connection;
  621. $credentials = array(
  622. 'ldapAgentName' => $name,
  623. 'ldapAgentPassword' => $password
  624. );
  625. if(!$testConnection->setConfiguration($credentials)) {
  626. return false;
  627. }
  628. return $testConnection->bind();
  629. }
  630. /**
  631. * @brief auto-detects the directory's UUID attribute
  632. * @param $dn a known DN used to check against
  633. * @param $force the detection should be run, even if it is not set to auto
  634. * @returns true on success, false otherwise
  635. */
  636. private function detectUuidAttribute($dn, $force = false) {
  637. if(($this->connection->ldapUuidAttribute != 'auto') && !$force) {
  638. return true;
  639. }
  640. //for now, supported (known) attributes are entryUUID, nsuniqueid, objectGUID
  641. $testAttributes = array('entryuuid', 'nsuniqueid', 'objectguid');
  642. foreach($testAttributes as $attribute) {
  643. \OCP\Util::writeLog('user_ldap', 'Testing '.$attribute.' as UUID attr', \OCP\Util::DEBUG);
  644. $value = $this->readAttribute($dn, $attribute);
  645. if(is_array($value) && isset($value[0]) && !empty($value[0])) {
  646. \OCP\Util::writeLog('user_ldap', 'Setting '.$attribute.' as UUID attr', \OCP\Util::DEBUG);
  647. $this->connection->ldapUuidAttribute = $attribute;
  648. return true;
  649. }
  650. \OCP\Util::writeLog('user_ldap', 'The looked for uuid attr is not '.$attribute.', result was '.print_r($value, true), \OCP\Util::DEBUG);
  651. }
  652. return false;
  653. }
  654. public function getUUID($dn) {
  655. if($this->detectUuidAttribute($dn)) {
  656. \OCP\Util::writeLog('user_ldap', 'UUID Checking \ UUID for '.$dn.' using '. $this->connection->ldapUuidAttribute, \OCP\Util::DEBUG);
  657. $uuid = $this->readAttribute($dn, $this->connection->ldapUuidAttribute);
  658. if(!is_array($uuid) && $this->connection->ldapOverrideUuidAttribute) {
  659. $this->detectUuidAttribute($dn, true);
  660. $uuid = $this->readAttribute($dn, $this->connection->ldapUuidAttribute);
  661. }
  662. if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
  663. $uuid = $uuid[0];
  664. } else {
  665. $uuid = false;
  666. }
  667. } else {
  668. $uuid = false;
  669. }
  670. return $uuid;
  671. }
  672. /**
  673. * @brief converts a binary ObjectGUID into a string representation
  674. * @param $oguid the ObjectGUID in it's binary form as retrieved from AD
  675. * @returns String
  676. *
  677. * converts a binary ObjectGUID into a string representation
  678. * http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
  679. */
  680. private function convertObjectGUID2Str($oguid) {
  681. $hex_guid = bin2hex($oguid);
  682. $hex_guid_to_guid_str = '';
  683. for($k = 1; $k <= 4; ++$k) {
  684. $hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
  685. }
  686. $hex_guid_to_guid_str .= '-';
  687. for($k = 1; $k <= 2; ++$k) {
  688. $hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
  689. }
  690. $hex_guid_to_guid_str .= '-';
  691. for($k = 1; $k <= 2; ++$k) {
  692. $hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
  693. }
  694. $hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
  695. $hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
  696. return strtoupper($hex_guid_to_guid_str);
  697. }
  698. /**
  699. * @brief converts a stored DN so it can be used as base parameter for LDAP queries
  700. * @param $dn the DN
  701. * @returns String
  702. *
  703. * converts a stored DN so it can be used as base parameter for LDAP queries
  704. * internally we store them for usage in LDAP filters
  705. */
  706. private function DNasBaseParameter($dn) {
  707. return str_replace('\\5c', '\\', $dn);
  708. }
  709. /**
  710. * @brief get a cookie for the next LDAP paged search
  711. * @param $filter the search filter to identify the correct search
  712. * @param $limit the limit (or 'pageSize'), to identify the correct search well
  713. * @param $offset the offset for the new search to identify the correct search really good
  714. * @returns string containing the key or empty if none is cached
  715. */
  716. private function getPagedResultCookie($filter, $limit, $offset) {
  717. if($offset == 0) {
  718. return '';
  719. }
  720. $offset -= $limit;
  721. //we work with cache here
  722. $cachekey = 'lc' . dechex(crc32($filter)) . '-' . $limit . '-' . $offset;
  723. $cookie = $this->connection->getFromCache($cachekey);
  724. if(is_null($cookie)) {
  725. $cookie = '';
  726. }
  727. return $cookie;
  728. }
  729. /**
  730. * @brief set a cookie for LDAP paged search run
  731. * @param $filter the search filter to identify the correct search
  732. * @param $limit the limit (or 'pageSize'), to identify the correct search well
  733. * @param $offset the offset for the run search to identify the correct search really good
  734. * @param $cookie string containing the cookie returned by ldap_control_paged_result_response
  735. * @return void
  736. */
  737. private function setPagedResultCookie($filter, $limit, $offset) {
  738. if(!empty($cookie)) {
  739. $cachekey = 'lc' . dechex(crc32($filter)) . '-' . $limit . '-' . $offset;
  740. $cookie = $this->connection->writeToCache($cachekey, $cookie);
  741. }
  742. }
  743. /**
  744. * @brief check wether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
  745. * @return true on success, null or false otherwise
  746. */
  747. public function getPagedSearchResultState() {
  748. $result = $this->pagedSearchedSuccessful;
  749. $this->pagedSearchedSuccessful = null;
  750. return $result;
  751. }
  752. /**
  753. * @brief prepares a paged search, if possible
  754. * @param $filter the LDAP filter for the search
  755. * @param $base the LDAP subtree that shall be searched
  756. * @param $attr optional, when a certain attribute shall be filtered outside
  757. * @param $limit
  758. * @param $offset
  759. *
  760. */
  761. private function initPagedSearch($filter, $base, $attr, $limit, $offset) {
  762. $pagedSearchOK = false;
  763. if($this->connection->hasPagedResultSupport && !is_null($limit)) {
  764. $offset = intval($offset); //can be null
  765. \OCP\Util::writeLog('user_ldap', 'initializing paged search for Filter'.$filter.' base '.$base.' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset, \OCP\Util::DEBUG);
  766. //get the cookie from the search for the previous search, required by LDAP
  767. $cookie = $this->getPagedResultCookie($filter, $limit, $offset);
  768. if(empty($cookie) && ($offset > 0)) {
  769. //no cookie known, although the offset is not 0. Maybe cache run out. We need to start all over *sigh* (btw, Dear Reader, did you need LDAP paged searching was designed by MSFT?)
  770. $reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
  771. //a bit recursive, $offset of 0 is the exit
  772. \OCP\Util::writeLog('user_ldap', 'Looking for cookie L/O '.$limit.'/'.$reOffset, \OCP\Util::INFO);
  773. $this->search($filter, $base, $attr, $limit, $reOffset, true);
  774. $cookie = $this->getPagedResultCookie($filter, $limit, $offset);
  775. //still no cookie? obviously, the server does not like us. Let's skip paging efforts.
  776. //TODO: remember this, probably does not change in the next request...
  777. if(empty($cookie)) {
  778. $cookie = null;
  779. }
  780. }
  781. if(!is_null($cookie)) {
  782. if($offset > 0) {
  783. \OCP\Util::writeLog('user_ldap', 'Cookie '.$cookie, \OCP\Util::INFO);
  784. }
  785. $pagedSearchOK = ldap_control_paged_result($this->connection->getConnectionResource(), $limit, false, $cookie);
  786. \OCP\Util::writeLog('user_ldap', 'Ready for a paged search', \OCP\Util::INFO);
  787. } else {
  788. \OCP\Util::writeLog('user_ldap', 'No paged search for us, Cpt., Limit '.$limit.' Offset '.$offset, \OCP\Util::INFO);
  789. }
  790. }
  791. return $pagedSearchOK;
  792. }
  793. }