access.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877
  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. $aDN = ldap_explode_dn($dn, false);
  116. unset($aDN['count']);
  117. $dn = implode(',', $aDN);
  118. $dn = str_replace('\\', '\\5c', $dn);
  119. return $dn;
  120. }
  121. /**
  122. * gives back the database table for the query
  123. */
  124. private function getMapTable($isUser) {
  125. if($isUser) {
  126. return '*PREFIX*ldap_user_mapping';
  127. } else {
  128. return '*PREFIX*ldap_group_mapping';
  129. }
  130. }
  131. /**
  132. * @brief returns the LDAP DN for the given internal ownCloud name of the group
  133. * @param $name the ownCloud name in question
  134. * @returns string with the LDAP DN on success, otherwise false
  135. *
  136. * returns the LDAP DN for the given internal ownCloud name of the group
  137. */
  138. public function groupname2dn($name) {
  139. $dn = $this->ocname2dn($name, false);
  140. if($dn) {
  141. return $dn;
  142. }
  143. return false;
  144. }
  145. /**
  146. * @brief returns the LDAP DN for the given internal ownCloud name of the user
  147. * @param $name the ownCloud name in question
  148. * @returns string with the LDAP DN on success, otherwise false
  149. *
  150. * returns the LDAP DN for the given internal ownCloud name of the user
  151. */
  152. public function username2dn($name) {
  153. $dn = $this->ocname2dn($name, true);
  154. if($dn) {
  155. return $dn;
  156. }
  157. return false;
  158. }
  159. /**
  160. * @brief returns the LDAP DN for the given internal ownCloud name
  161. * @param $name the ownCloud name in question
  162. * @param $isUser is it a user? otherwise group
  163. * @returns string with the LDAP DN on success, otherwise false
  164. *
  165. * returns the LDAP DN for the given internal ownCloud name
  166. */
  167. private function ocname2dn($name, $isUser) {
  168. $table = $this->getMapTable($isUser);
  169. $query = \OCP\DB::prepare('
  170. SELECT `ldap_dn`
  171. FROM `'.$table.'`
  172. WHERE `owncloud_name` = ?
  173. ');
  174. $record = $query->execute(array($name))->fetchOne();
  175. return $record;
  176. }
  177. /**
  178. * @brief returns the internal ownCloud name for the given LDAP DN of the group
  179. * @param $dn the dn of the group object
  180. * @param $ldapname optional, the display name of the object
  181. * @returns string with with the name to use in ownCloud, false on DN outside of search DN
  182. *
  183. * returns the internal ownCloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
  184. */
  185. public function dn2groupname($dn, $ldapname = null) {
  186. 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'))) {
  187. return false;
  188. }
  189. return $this->dn2ocname($dn, $ldapname, false);
  190. }
  191. /**
  192. * @brief returns the internal ownCloud name for the given LDAP DN of the user
  193. * @param $dn the dn of the user object
  194. * @param $ldapname optional, the display name of the object
  195. * @returns string with with the name to use in ownCloud
  196. *
  197. * returns the internal ownCloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
  198. */
  199. public function dn2username($dn, $ldapname = null) {
  200. 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'))) {
  201. return false;
  202. }
  203. return $this->dn2ocname($dn, $ldapname, true);
  204. }
  205. /**
  206. * @brief returns an internal ownCloud name for the given LDAP DN
  207. * @param $dn the dn of the user object
  208. * @param $ldapname optional, the display name of the object
  209. * @param $isUser optional, wether it is a user object (otherwise group assumed)
  210. * @returns string with with the name to use in ownCloud
  211. *
  212. * returns the internal ownCloud name for the given LDAP DN of the user, false on DN outside of search DN
  213. */
  214. public function dn2ocname($dn, $ldapname = null, $isUser = true) {
  215. $table = $this->getMapTable($isUser);
  216. if($isUser) {
  217. $fncFindMappedName = 'findMappedUser';
  218. $nameAttribute = $this->connection->ldapUserDisplayName;
  219. } else {
  220. $fncFindMappedName = 'findMappedGroup';
  221. $nameAttribute = $this->connection->ldapGroupDisplayName;
  222. }
  223. //let's try to retrieve the ownCloud name from the mappings table
  224. $ocname = $this->$fncFindMappedName($dn);
  225. if($ocname) {
  226. return $ocname;
  227. }
  228. //second try: get the UUID and check if it is known. Then, update the DN and return the name.
  229. $uuid = $this->getUUID($dn);
  230. if($uuid) {
  231. $query = \OCP\DB::prepare('
  232. SELECT `owncloud_name`
  233. FROM `'.$table.'`
  234. WHERE `directory_uuid` = ?
  235. ');
  236. $component = $query->execute(array($uuid))->fetchOne();
  237. if($component) {
  238. $query = \OCP\DB::prepare('
  239. UPDATE `'.$table.'`
  240. SET `ldap_dn` = ?
  241. WHERE `directory_uuid` = ?
  242. ');
  243. $query->execute(array($dn, $uuid));
  244. return $component;
  245. }
  246. }
  247. if(is_null($ldapname)) {
  248. $ldapname = $this->readAttribute($dn, $nameAttribute);
  249. if(!isset($ldapname[0]) && empty($ldapname[0])) {
  250. \OCP\Util::writeLog('user_ldap', 'No or empty name for '.$dn.'.', \OCP\Util::INFO);
  251. return false;
  252. }
  253. $ldapname = $ldapname[0];
  254. }
  255. $ldapname = $this->sanitizeUsername($ldapname);
  256. //a new user/group! Then let's try to add it. We're shooting into the blue with the user/group name, assuming that in most cases there will not be a conflict. Otherwise an error will occur and we will continue with our second shot.
  257. if(($isUser && !\OCP\User::userExists($ldapname)) || (!$isUser && !\OC_Group::groupExists($ldapname))) {
  258. if($this->mapComponent($dn, $ldapname, $isUser)) {
  259. return $ldapname;
  260. }
  261. }
  262. //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.
  263. $oc_name = $this->alternateOwnCloudName($ldapname, $dn);
  264. if(($isUser && !\OCP\User::userExists($oc_name)) || (!$isUser && !\OC_Group::groupExists($oc_name))) {
  265. if($this->mapComponent($dn, $oc_name, $isUser)) {
  266. return $oc_name;
  267. }
  268. }
  269. //if everything else did not help..
  270. \OCP\Util::writeLog('user_ldap', 'Could not create unique ownCloud name for '.$dn.'.', \OCP\Util::INFO);
  271. return false;
  272. }
  273. /**
  274. * @brief gives back the user names as they are used ownClod internally
  275. * @param $ldapGroups an array with the ldap Users result in style of array ( array ('dn' => foo, 'uid' => bar), ... )
  276. * @returns an array with the user names to use in ownCloud
  277. *
  278. * gives back the user names as they are used ownClod internally
  279. */
  280. public function ownCloudUserNames($ldapUsers) {
  281. return $this->ldap2ownCloudNames($ldapUsers, true);
  282. }
  283. /**
  284. * @brief gives back the group names as they are used ownClod internally
  285. * @param $ldapGroups an array with the ldap Groups result in style of array ( array ('dn' => foo, 'cn' => bar), ... )
  286. * @returns an array with the group names to use in ownCloud
  287. *
  288. * gives back the group names as they are used ownClod internally
  289. */
  290. public function ownCloudGroupNames($ldapGroups) {
  291. return $this->ldap2ownCloudNames($ldapGroups, false);
  292. }
  293. private function findMappedUser($dn) {
  294. static $query = null;
  295. if(is_null($query)) {
  296. $query = \OCP\DB::prepare('
  297. SELECT `owncloud_name`
  298. FROM `'.$this->getMapTable(true).'`
  299. WHERE `ldap_dn` = ?'
  300. );
  301. }
  302. $res = $query->execute(array($dn))->fetchOne();
  303. if($res) {
  304. return $res;
  305. }
  306. return false;
  307. }
  308. private function findMappedGroup($dn) {
  309. static $query = null;
  310. if(is_null($query)) {
  311. $query = \OCP\DB::prepare('
  312. SELECT `owncloud_name`
  313. FROM `'.$this->getMapTable(false).'`
  314. WHERE `ldap_dn` = ?'
  315. );
  316. }
  317. $res = $query->execute(array($dn))->fetchOne();
  318. if($res) {
  319. return $res;
  320. }
  321. return false;
  322. }
  323. private function ldap2ownCloudNames($ldapObjects, $isUsers) {
  324. if($isUsers) {
  325. $nameAttribute = $this->connection->ldapUserDisplayName;
  326. } else {
  327. $nameAttribute = $this->connection->ldapGroupDisplayName;
  328. }
  329. $ownCloudNames = array();
  330. foreach($ldapObjects as $ldapObject) {
  331. $nameByLDAP = isset($ldapObject[$nameAttribute]) ? $ldapObject[$nameAttribute] : null;
  332. $ocname = $this->dn2ocname($ldapObject['dn'], $nameByLDAP, $isUsers);
  333. if($ocname) {
  334. $ownCloudNames[] = $ocname;
  335. }
  336. continue;
  337. }
  338. return $ownCloudNames;
  339. }
  340. /**
  341. * @brief creates a hopefully unique name for owncloud based on the display name and the dn of the LDAP object
  342. * @param $name the display name of the object
  343. * @param $dn the dn of the object
  344. * @returns string with with the name to use in ownCloud
  345. *
  346. * creates a hopefully unique name for owncloud based on the display name and the dn of the LDAP object
  347. */
  348. private function alternateOwnCloudName($name, $dn) {
  349. $ufn = ldap_dn2ufn($dn);
  350. $name = $name . '@' . trim(\OCP\Util::mb_substr_replace($ufn, '', 0, mb_strpos($ufn, ',', 0, 'UTF-8'), 'UTF-8'));
  351. $name = $this->sanitizeUsername($name);
  352. return $name;
  353. }
  354. /**
  355. * @brief retrieves all known groups from the mappings table
  356. * @returns array with the results
  357. *
  358. * retrieves all known groups from the mappings table
  359. */
  360. private function mappedGroups() {
  361. return $this->mappedComponents(false);
  362. }
  363. /**
  364. * @brief retrieves all known users from the mappings table
  365. * @returns array with the results
  366. *
  367. * retrieves all known users from the mappings table
  368. */
  369. private function mappedUsers() {
  370. return $this->mappedComponents(true);
  371. }
  372. private function mappedComponents($isUsers) {
  373. $table = $this->getMapTable($isUsers);
  374. $query = \OCP\DB::prepare('
  375. SELECT `ldap_dn`, `owncloud_name`
  376. FROM `'. $table . '`'
  377. );
  378. return $query->execute()->fetchAll();
  379. }
  380. /**
  381. * @brief inserts a new user or group into the mappings table
  382. * @param $dn the record in question
  383. * @param $ocname the name to use in ownCloud
  384. * @param $isUser is it a user or a group?
  385. * @returns true on success, false otherwise
  386. *
  387. * inserts a new user or group into the mappings table
  388. */
  389. private function mapComponent($dn, $ocname, $isUser = true) {
  390. $table = $this->getMapTable($isUser);
  391. $sqlAdjustment = '';
  392. $dbtype = \OCP\Config::getSystemValue('dbtype');
  393. if($dbtype == 'mysql') {
  394. $sqlAdjustment = 'FROM DUAL';
  395. }
  396. $insert = \OCP\DB::prepare('
  397. INSERT INTO `'.$table.'` (`ldap_dn`, `owncloud_name`, `directory_uuid`)
  398. SELECT ?,?,?
  399. '.$sqlAdjustment.'
  400. WHERE NOT EXISTS (
  401. SELECT 1
  402. FROM `'.$table.'`
  403. WHERE `ldap_dn` = ?
  404. OR `owncloud_name` = ?)
  405. ');
  406. //feed the DB
  407. $res = $insert->execute(array($dn, $ocname, $this->getUUID($dn), $dn, $ocname));
  408. if(\OCP\DB::isError($res)) {
  409. return false;
  410. }
  411. $insRows = $res->numRows();
  412. if($insRows == 0) {
  413. return false;
  414. }
  415. return true;
  416. }
  417. public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null) {
  418. return $this->fetchList($this->searchUsers($filter, $attr, $limit, $offset), (count($attr) > 1));
  419. }
  420. public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
  421. return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), (count($attr) > 1));
  422. }
  423. private function fetchList($list, $manyAttributes) {
  424. if(is_array($list)) {
  425. if($manyAttributes) {
  426. return $list;
  427. } else {
  428. return array_unique($list, SORT_LOCALE_STRING);
  429. }
  430. }
  431. //error cause actually, maybe throw an exception in future.
  432. return array();
  433. }
  434. /**
  435. * @brief executes an LDAP search, optimized for Users
  436. * @param $filter the LDAP filter for the search
  437. * @param $attr optional, when a certain attribute shall be filtered out
  438. * @returns array with the search result
  439. *
  440. * Executes an LDAP search
  441. */
  442. public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
  443. return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
  444. }
  445. /**
  446. * @brief executes an LDAP search, optimized for Groups
  447. * @param $filter the LDAP filter for the search
  448. * @param $attr optional, when a certain attribute shall be filtered out
  449. * @returns array with the search result
  450. *
  451. * Executes an LDAP search
  452. */
  453. public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
  454. return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
  455. }
  456. /**
  457. * @brief executes an LDAP search
  458. * @param $filter the LDAP filter for the search
  459. * @param $base the LDAP subtree that shall be searched
  460. * @param $attr optional, when a certain attribute shall be filtered out
  461. * @returns array with the search result
  462. *
  463. * Executes an LDAP search
  464. */
  465. private function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
  466. if(!is_null($attr) && !is_array($attr)) {
  467. $attr = array(mb_strtolower($attr, 'UTF-8'));
  468. }
  469. // See if we have a resource, in case not cancel with message
  470. $link_resource = $this->connection->getConnectionResource();
  471. if(!is_resource($link_resource)) {
  472. // Seems like we didn't find any resource.
  473. // Return an empty array just like before.
  474. \OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', \OCP\Util::DEBUG);
  475. return array();
  476. }
  477. //check wether paged search should be attempted
  478. $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, $limit, $offset);
  479. $sr = ldap_search($link_resource, $base, $filter, $attr);
  480. if(!$sr) {
  481. \OCP\Util::writeLog('user_ldap', 'Error when searching: '.ldap_error($link_resource).' code '.ldap_errno($link_resource), \OCP\Util::ERROR);
  482. \OCP\Util::writeLog('user_ldap', 'Attempt for Paging? '.print_r($pagedSearchOK, true), \OCP\Util::ERROR);
  483. return array();
  484. }
  485. $findings = ldap_get_entries($link_resource, $sr );
  486. if($pagedSearchOK) {
  487. \OCP\Util::writeLog('user_ldap', 'Paged search successful', \OCP\Util::INFO);
  488. ldap_control_paged_result_response($link_resource, $sr, $cookie);
  489. \OCP\Util::writeLog('user_ldap', 'Set paged search cookie '.$cookie, \OCP\Util::INFO);
  490. $this->setPagedResultCookie($filter, $limit, $offset, $cookie);
  491. //browsing through prior pages to get the cookie for the new one
  492. if($skipHandling) {
  493. return;
  494. }
  495. //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.
  496. if($findings['count'] <= $limit) {
  497. $this->pagedSearchedSuccessful = true;
  498. }
  499. } else {
  500. \OCP\Util::writeLog('user_ldap', 'Paged search failed :(', \OCP\Util::INFO);
  501. }
  502. // if we're here, probably no connection resource is returned.
  503. // to make ownCloud behave nicely, we simply give back an empty array.
  504. if(is_null($findings)) {
  505. return array();
  506. }
  507. if(!is_null($attr)) {
  508. $selection = array();
  509. $multiarray = false;
  510. if(count($attr) > 1) {
  511. $multiarray = true;
  512. $i = 0;
  513. }
  514. foreach($findings as $item) {
  515. if(!is_array($item)) {
  516. continue;
  517. }
  518. $item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
  519. if($multiarray) {
  520. foreach($attr as $key) {
  521. $key = mb_strtolower($key, 'UTF-8');
  522. if(isset($item[$key])) {
  523. if($key != 'dn') {
  524. $selection[$i][$key] = $this->resemblesDN($key) ? $this->sanitizeDN($item[$key][0]) : $item[$key][0];
  525. } else {
  526. $selection[$i][$key] = $this->sanitizeDN($item[$key]);
  527. }
  528. }
  529. }
  530. $i++;
  531. } else {
  532. //tribute to case insensitivity
  533. $key = mb_strtolower($attr[0], 'UTF-8');
  534. if(isset($item[$key])) {
  535. if($this->resemblesDN($key)) {
  536. $selection[] = $this->sanitizeDN($item[$key]);
  537. } else {
  538. $selection[] = $item[$key];
  539. }
  540. }
  541. }
  542. }
  543. $findings = $selection;
  544. }
  545. //we slice the findings, when
  546. //a) paged search insuccessful, though attempted
  547. //b) no paged search, but limit set
  548. if((!$this->pagedSearchedSuccessful
  549. && $pagedSearchOK)
  550. || (
  551. !$pagedSearchOK
  552. && !is_null($limit)
  553. )
  554. ) {
  555. $findings = array_slice($findings, intval($offset), $limit);
  556. }
  557. return $findings;
  558. }
  559. public function sanitizeUsername($name) {
  560. if($this->connection->ldapIgnoreNamingRules) {
  561. return $name;
  562. }
  563. // Translitaration
  564. //latin characters to ASCII
  565. $name = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
  566. //REPLACEMENTS
  567. $name = \OCP\Util::mb_str_replace(' ', '_', $name, 'UTF-8');
  568. //every remaining unallowed characters will be removed
  569. $name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
  570. return $name;
  571. }
  572. /**
  573. * @brief combines the input filters with AND
  574. * @param $filters array, the filters to connect
  575. * @returns the combined filter
  576. *
  577. * Combines Filter arguments with AND
  578. */
  579. public function combineFilterWithAnd($filters) {
  580. return $this->combineFilter($filters, '&');
  581. }
  582. /**
  583. * @brief combines the input filters with AND
  584. * @param $filters array, the filters to connect
  585. * @returns the combined filter
  586. *
  587. * Combines Filter arguments with AND
  588. */
  589. public function combineFilterWithOr($filters) {
  590. return $this->combineFilter($filters, '|');
  591. }
  592. /**
  593. * @brief combines the input filters with given operator
  594. * @param $filters array, the filters to connect
  595. * @param $operator either & or |
  596. * @returns the combined filter
  597. *
  598. * Combines Filter arguments with AND
  599. */
  600. private function combineFilter($filters, $operator) {
  601. $combinedFilter = '('.$operator;
  602. foreach($filters as $filter) {
  603. if($filter[0] != '(') {
  604. $filter = '('.$filter.')';
  605. }
  606. $combinedFilter.=$filter;
  607. }
  608. $combinedFilter.=')';
  609. return $combinedFilter;
  610. }
  611. public function areCredentialsValid($name, $password) {
  612. $name = $this->DNasBaseParameter($name);
  613. $testConnection = clone $this->connection;
  614. $credentials = array(
  615. 'ldapAgentName' => $name,
  616. 'ldapAgentPassword' => $password
  617. );
  618. if(!$testConnection->setConfiguration($credentials)) {
  619. return false;
  620. }
  621. return $testConnection->bind();
  622. }
  623. /**
  624. * @brief auto-detects the directory's UUID attribute
  625. * @param $dn a known DN used to check against
  626. * @param $force the detection should be run, even if it is not set to auto
  627. * @returns true on success, false otherwise
  628. */
  629. private function detectUuidAttribute($dn, $force = false) {
  630. if(($this->connection->ldapUuidAttribute != 'auto') && !$force) {
  631. return true;
  632. }
  633. //for now, supported (known) attributes are entryUUID, nsuniqueid, objectGUID
  634. $testAttributes = array('entryuuid', 'nsuniqueid', 'objectguid');
  635. foreach($testAttributes as $attribute) {
  636. \OCP\Util::writeLog('user_ldap', 'Testing '.$attribute.' as UUID attr', \OCP\Util::DEBUG);
  637. $value = $this->readAttribute($dn, $attribute);
  638. if(is_array($value) && isset($value[0]) && !empty($value[0])) {
  639. \OCP\Util::writeLog('user_ldap', 'Setting '.$attribute.' as UUID attr', \OCP\Util::DEBUG);
  640. $this->connection->ldapUuidAttribute = $attribute;
  641. return true;
  642. }
  643. \OCP\Util::writeLog('user_ldap', 'The looked for uuid attr is not '.$attribute.', result was '.print_r($value, true), \OCP\Util::DEBUG);
  644. }
  645. return false;
  646. }
  647. public function getUUID($dn) {
  648. if($this->detectUuidAttribute($dn)) {
  649. \OCP\Util::writeLog('user_ldap', 'UUID Checking \ UUID for '.$dn.' using '. $this->connection->ldapUuidAttribute, \OCP\Util::DEBUG);
  650. $uuid = $this->readAttribute($dn, $this->connection->ldapUuidAttribute);
  651. if(!is_array($uuid) && $this->connection->ldapOverrideUuidAttribute) {
  652. $this->detectUuidAttribute($dn, true);
  653. $uuid = $this->readAttribute($dn, $this->connection->ldapUuidAttribute);
  654. }
  655. if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
  656. $uuid = $uuid[0];
  657. } else {
  658. $uuid = false;
  659. }
  660. } else {
  661. $uuid = false;
  662. }
  663. return $uuid;
  664. }
  665. /**
  666. * @brief converts a binary ObjectGUID into a string representation
  667. * @param $oguid the ObjectGUID in it's binary form as retrieved from AD
  668. * @returns String
  669. *
  670. * converts a binary ObjectGUID into a string representation
  671. * http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
  672. */
  673. private function convertObjectGUID2Str($oguid) {
  674. $hex_guid = bin2hex($oguid);
  675. $hex_guid_to_guid_str = '';
  676. for($k = 1; $k <= 4; ++$k) {
  677. $hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
  678. }
  679. $hex_guid_to_guid_str .= '-';
  680. for($k = 1; $k <= 2; ++$k) {
  681. $hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
  682. }
  683. $hex_guid_to_guid_str .= '-';
  684. for($k = 1; $k <= 2; ++$k) {
  685. $hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
  686. }
  687. $hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
  688. $hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
  689. return strtoupper($hex_guid_to_guid_str);
  690. }
  691. /**
  692. * @brief converts a stored DN so it can be used as base parameter for LDAP queries
  693. * @param $dn the DN
  694. * @returns String
  695. *
  696. * converts a stored DN so it can be used as base parameter for LDAP queries
  697. * internally we store them for usage in LDAP filters
  698. */
  699. private function DNasBaseParameter($dn) {
  700. return str_replace('\\5c', '\\', $dn);
  701. }
  702. /**
  703. * @brief get a cookie for the next LDAP paged search
  704. * @param $filter the search filter to identify the correct search
  705. * @param $limit the limit (or 'pageSize'), to identify the correct search well
  706. * @param $offset the offset for the new search to identify the correct search really good
  707. * @returns string containing the key or empty if none is cached
  708. */
  709. private function getPagedResultCookie($filter, $limit, $offset) {
  710. if($offset == 0) {
  711. return '';
  712. }
  713. $offset -= $limit;
  714. //we work with cache here
  715. $cachekey = 'lc' . dechex(crc32($filter)) . '-' . $limit . '-' . $offset;
  716. $cookie = $this->connection->getFromCache($cachekey);
  717. if(is_null($cookie)) {
  718. $cookie = '';
  719. }
  720. return $cookie;
  721. }
  722. /**
  723. * @brief set a cookie for LDAP paged search run
  724. * @param $filter the search filter to identify the correct search
  725. * @param $limit the limit (or 'pageSize'), to identify the correct search well
  726. * @param $offset the offset for the run search to identify the correct search really good
  727. * @param $cookie string containing the cookie returned by ldap_control_paged_result_response
  728. * @return void
  729. */
  730. private function setPagedResultCookie($filter, $limit, $offset) {
  731. if(!empty($cookie)) {
  732. $cachekey = 'lc' . dechex(crc32($filter)) . '-' . $limit . '-' . $offset;
  733. $cookie = $this->connection->writeToCache($cachekey, $cookie);
  734. }
  735. }
  736. /**
  737. * @brief check wether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
  738. * @return true on success, null or false otherwise
  739. */
  740. public function getPagedSearchResultState() {
  741. $result = $this->pagedSearchedSuccessful;
  742. $this->pagedSearchedSuccessful = null;
  743. return $result;
  744. }
  745. /**
  746. * @brief prepares a paged search, if possible
  747. * @param $filter the LDAP filter for the search
  748. * @param $base the LDAP subtree that shall be searched
  749. * @param $attr optional, when a certain attribute shall be filtered outside
  750. * @param $limit
  751. * @param $offset
  752. *
  753. */
  754. private function initPagedSearch($filter, $base, $attr, $limit, $offset) {
  755. $pagedSearchOK = false;
  756. if($this->connection->hasPagedResultSupport && !is_null($limit)) {
  757. $offset = intval($offset); //can be null
  758. \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);
  759. //get the cookie from the search for the previous search, required by LDAP
  760. $cookie = $this->getPagedResultCookie($filter, $limit, $offset);
  761. if(empty($cookie) && ($offset > 0)) {
  762. //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?)
  763. $reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
  764. //a bit recursive, $offset of 0 is the exit
  765. \OCP\Util::writeLog('user_ldap', 'Looking for cookie L/O '.$limit.'/'.$reOffset, \OCP\Util::INFO);
  766. $this->search($filter, $base, $attr, $limit, $reOffset, true);
  767. $cookie = $this->getPagedResultCookie($filter, $limit, $offset);
  768. //still no cookie? obviously, the server does not like us. Let's skip paging efforts.
  769. //TODO: remember this, probably does not change in the next request...
  770. if(empty($cookie)) {
  771. $cookie = null;
  772. }
  773. }
  774. if(!is_null($cookie)) {
  775. if($offset > 0) {
  776. \OCP\Util::writeLog('user_ldap', 'Cookie '.$cookie, \OCP\Util::INFO);
  777. }
  778. $pagedSearchOK = ldap_control_paged_result($this->connection->getConnectionResource(), $limit, false, $cookie);
  779. \OCP\Util::writeLog('user_ldap', 'Ready for a paged search', \OCP\Util::INFO);
  780. } else {
  781. \OCP\Util::writeLog('user_ldap', 'No paged search for us, Cpt., Limit '.$limit.' Offset '.$offset, \OCP\Util::INFO);
  782. }
  783. }
  784. return $pagedSearchOK;
  785. }
  786. }