access.php 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476
  1. <?php
  2. /**
  3. * @author Alexander Bergolth <leo@strike.wu.ac.at>
  4. * @author Andreas Fischer <bantu@owncloud.com>
  5. * @author Arthur Schiwon <blizzz@owncloud.com>
  6. * @author Bart Visscher <bartv@thisnet.nl>
  7. * @author Benjamin Diele <benjamin@diele.be>
  8. * @author Christopher Schäpers <kondou@ts.unde.re>
  9. * @author Donald Buczek <buczek@molgen.mpg.de>
  10. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  11. * @author Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>
  12. * @author Lukas Reschke <lukas@owncloud.com>
  13. * @author Lyonel Vincent <lyonel@ezix.org>
  14. * @author Morris Jobke <hey@morrisjobke.de>
  15. * @author Robin McCorkell <rmccorkell@karoshi.org.uk>
  16. * @author Scrutinizer Auto-Fixer <auto-fixer@scrutinizer-ci.com>
  17. *
  18. * @copyright Copyright (c) 2015, ownCloud, Inc.
  19. * @license AGPL-3.0
  20. *
  21. * This code is free software: you can redistribute it and/or modify
  22. * it under the terms of the GNU Affero General Public License, version 3,
  23. * as published by the Free Software Foundation.
  24. *
  25. * This program is distributed in the hope that it will be useful,
  26. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  27. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  28. * GNU Affero General Public License for more details.
  29. *
  30. * You should have received a copy of the GNU Affero General Public License, version 3,
  31. * along with this program. If not, see <http://www.gnu.org/licenses/>
  32. *
  33. */
  34. namespace OCA\user_ldap\lib;
  35. use OCA\User_LDAP\Mapping\AbstractMapping;
  36. /**
  37. * Class Access
  38. * @package OCA\user_ldap\lib
  39. */
  40. class Access extends LDAPUtility implements user\IUserTools {
  41. /**
  42. * @var \OCA\user_ldap\lib\Connection
  43. */
  44. public $connection;
  45. public $userManager;
  46. //never ever check this var directly, always use getPagedSearchResultState
  47. protected $pagedSearchedSuccessful;
  48. /**
  49. * @var string[] $cookies an array of returned Paged Result cookies
  50. */
  51. protected $cookies = array();
  52. /**
  53. * @var string $lastCookie the last cookie returned from a Paged Results
  54. * operation, defaults to an empty string
  55. */
  56. protected $lastCookie = '';
  57. /**
  58. * @var AbstractMapping $userMapper
  59. */
  60. protected $userMapper;
  61. /**
  62. * @var AbstractMapping $userMapper
  63. */
  64. protected $groupMapper;
  65. public function __construct(Connection $connection, ILDAPWrapper $ldap,
  66. user\Manager $userManager) {
  67. parent::__construct($ldap);
  68. $this->connection = $connection;
  69. $this->userManager = $userManager;
  70. $this->userManager->setLdapAccess($this);
  71. }
  72. /**
  73. * sets the User Mapper
  74. * @param AbstractMapping $mapper
  75. */
  76. public function setUserMapper(AbstractMapping $mapper) {
  77. $this->userMapper = $mapper;
  78. }
  79. /**
  80. * returns the User Mapper
  81. * @throws \Exception
  82. * @return AbstractMapping
  83. */
  84. public function getUserMapper() {
  85. if(is_null($this->userMapper)) {
  86. throw new \Exception('UserMapper was not assigned to this Access instance.');
  87. }
  88. return $this->userMapper;
  89. }
  90. /**
  91. * sets the Group Mapper
  92. * @param AbstractMapping $mapper
  93. */
  94. public function setGroupMapper(AbstractMapping $mapper) {
  95. $this->groupMapper = $mapper;
  96. }
  97. /**
  98. * returns the Group Mapper
  99. * @throws \Exception
  100. * @return AbstractMapping
  101. */
  102. public function getGroupMapper() {
  103. if(is_null($this->groupMapper)) {
  104. throw new \Exception('GroupMapper was not assigned to this Access instance.');
  105. }
  106. return $this->groupMapper;
  107. }
  108. /**
  109. * @return bool
  110. */
  111. private function checkConnection() {
  112. return ($this->connection instanceof Connection);
  113. }
  114. /**
  115. * returns the Connection instance
  116. * @return \OCA\user_ldap\lib\Connection
  117. */
  118. public function getConnection() {
  119. return $this->connection;
  120. }
  121. /**
  122. * reads a given attribute for an LDAP record identified by a DN
  123. * @param string $dn the record in question
  124. * @param string $attr the attribute that shall be retrieved
  125. * if empty, just check the record's existence
  126. * @param string $filter
  127. * @return array|false an array of values on success or an empty
  128. * array if $attr is empty, false otherwise
  129. */
  130. public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
  131. if(!$this->checkConnection()) {
  132. \OCP\Util::writeLog('user_ldap',
  133. 'No LDAP Connector assigned, access impossible for readAttribute.',
  134. \OCP\Util::WARN);
  135. return false;
  136. }
  137. $cr = $this->connection->getConnectionResource();
  138. if(!$this->ldap->isResource($cr)) {
  139. //LDAP not available
  140. \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
  141. return false;
  142. }
  143. //Cancel possibly running Paged Results operation, otherwise we run in
  144. //LDAP protocol errors
  145. $this->abandonPagedSearch();
  146. // openLDAP requires that we init a new Paged Search. Not needed by AD,
  147. // but does not hurt either.
  148. $pagingSize = intval($this->connection->ldapPagingSize);
  149. // 0 won't result in replies, small numbers may leave out groups
  150. // (cf. #12306), 500 is default for paging and should work everywhere.
  151. $maxResults = $pagingSize > 20 ? $pagingSize : 500;
  152. $this->initPagedSearch($filter, array($dn), array($attr), $maxResults, 0);
  153. $dn = $this->DNasBaseParameter($dn);
  154. $rr = @$this->ldap->read($cr, $dn, $filter, array($attr));
  155. if(!$this->ldap->isResource($rr)) {
  156. if(!empty($attr)) {
  157. //do not throw this message on userExists check, irritates
  158. \OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN '.$dn, \OCP\Util::DEBUG);
  159. }
  160. //in case an error occurs , e.g. object does not exist
  161. return false;
  162. }
  163. if (empty($attr)) {
  164. \OCP\Util::writeLog('user_ldap', 'readAttribute: '.$dn.' found', \OCP\Util::DEBUG);
  165. return array();
  166. }
  167. $er = $this->ldap->firstEntry($cr, $rr);
  168. if(!$this->ldap->isResource($er)) {
  169. //did not match the filter, return false
  170. return false;
  171. }
  172. //LDAP attributes are not case sensitive
  173. $result = \OCP\Util::mb_array_change_key_case(
  174. $this->ldap->getAttributes($cr, $er), MB_CASE_LOWER, 'UTF-8');
  175. $attr = mb_strtolower($attr, 'UTF-8');
  176. if(isset($result[$attr]) && $result[$attr]['count'] > 0) {
  177. $values = array();
  178. for($i=0;$i<$result[$attr]['count'];$i++) {
  179. if($this->resemblesDN($attr)) {
  180. $values[] = $this->sanitizeDN($result[$attr][$i]);
  181. } elseif(strtolower($attr) === 'objectguid' || strtolower($attr) === 'guid') {
  182. $values[] = $this->convertObjectGUID2Str($result[$attr][$i]);
  183. } else {
  184. $values[] = $result[$attr][$i];
  185. }
  186. }
  187. return $values;
  188. }
  189. \OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, \OCP\Util::DEBUG);
  190. return false;
  191. }
  192. /**
  193. * checks whether the given attributes value is probably a DN
  194. * @param string $attr the attribute in question
  195. * @return boolean if so true, otherwise false
  196. */
  197. private function resemblesDN($attr) {
  198. $resemblingAttributes = array(
  199. 'dn',
  200. 'uniquemember',
  201. 'member'
  202. );
  203. return in_array($attr, $resemblingAttributes);
  204. }
  205. /**
  206. * checks whether the given string is probably a DN
  207. * @param string $string
  208. * @return boolean
  209. */
  210. public function stringResemblesDN($string) {
  211. $r = $this->ldap->explodeDN($string, 0);
  212. // if exploding a DN succeeds and does not end up in
  213. // an empty array except for $r[count] being 0.
  214. return (is_array($r) && count($r) > 1);
  215. }
  216. /**
  217. * sanitizes a DN received from the LDAP server
  218. * @param array $dn the DN in question
  219. * @return array the sanitized DN
  220. */
  221. private function sanitizeDN($dn) {
  222. //treating multiple base DNs
  223. if(is_array($dn)) {
  224. $result = array();
  225. foreach($dn as $singleDN) {
  226. $result[] = $this->sanitizeDN($singleDN);
  227. }
  228. return $result;
  229. }
  230. //OID sometimes gives back DNs with whitespace after the comma
  231. // a la "uid=foo, cn=bar, dn=..." We need to tackle this!
  232. $dn = preg_replace('/([^\\\]),(\s+)/u', '\1,', $dn);
  233. //make comparisons and everything work
  234. $dn = mb_strtolower($dn, 'UTF-8');
  235. //escape DN values according to RFC 2253 – this is already done by ldap_explode_dn
  236. //to use the DN in search filters, \ needs to be escaped to \5c additionally
  237. //to use them in bases, we convert them back to simple backslashes in readAttribute()
  238. $replacements = array(
  239. '\,' => '\5c2C',
  240. '\=' => '\5c3D',
  241. '\+' => '\5c2B',
  242. '\<' => '\5c3C',
  243. '\>' => '\5c3E',
  244. '\;' => '\5c3B',
  245. '\"' => '\5c22',
  246. '\#' => '\5c23',
  247. '(' => '\28',
  248. ')' => '\29',
  249. '*' => '\2A',
  250. );
  251. $dn = str_replace(array_keys($replacements), array_values($replacements), $dn);
  252. return $dn;
  253. }
  254. /**
  255. * returns a DN-string that is cleaned from not domain parts, e.g.
  256. * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
  257. * becomes dc=foobar,dc=server,dc=org
  258. * @param string $dn
  259. * @return string
  260. */
  261. public function getDomainDNFromDN($dn) {
  262. $allParts = $this->ldap->explodeDN($dn, 0);
  263. if($allParts === false) {
  264. //not a valid DN
  265. return '';
  266. }
  267. $domainParts = array();
  268. $dcFound = false;
  269. foreach($allParts as $part) {
  270. if(!$dcFound && strpos($part, 'dc=') === 0) {
  271. $dcFound = true;
  272. }
  273. if($dcFound) {
  274. $domainParts[] = $part;
  275. }
  276. }
  277. $domainDN = implode(',', $domainParts);
  278. return $domainDN;
  279. }
  280. /**
  281. * returns the LDAP DN for the given internal ownCloud name of the group
  282. * @param string $name the ownCloud name in question
  283. * @return string|false LDAP DN on success, otherwise false
  284. */
  285. public function groupname2dn($name) {
  286. return $this->groupMapper->getDNbyName($name);
  287. }
  288. /**
  289. * returns the LDAP DN for the given internal ownCloud name of the user
  290. * @param string $name the ownCloud name in question
  291. * @return string|false with the LDAP DN on success, otherwise false
  292. */
  293. public function username2dn($name) {
  294. $fdn = $this->userMapper->getDNbyName($name);
  295. //Check whether the DN belongs to the Base, to avoid issues on multi-
  296. //server setups
  297. if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
  298. return $fdn;
  299. }
  300. return false;
  301. }
  302. /**
  303. public function ocname2dn($name, $isUser) {
  304. * returns the internal ownCloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
  305. * @param string $fdn the dn of the group object
  306. * @param string $ldapName optional, the display name of the object
  307. * @return string|false with the name to use in ownCloud, false on DN outside of search DN
  308. */
  309. public function dn2groupname($fdn, $ldapName = null) {
  310. //To avoid bypassing the base DN settings under certain circumstances
  311. //with the group support, check whether the provided DN matches one of
  312. //the given Bases
  313. if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
  314. return false;
  315. }
  316. return $this->dn2ocname($fdn, $ldapName, false);
  317. }
  318. /**
  319. * returns the internal ownCloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
  320. * @param string $dn the dn of the user object
  321. * @param string $ldapName optional, the display name of the object
  322. * @return string|false with with the name to use in ownCloud
  323. */
  324. public function dn2username($fdn, $ldapName = null) {
  325. //To avoid bypassing the base DN settings under certain circumstances
  326. //with the group support, check whether the provided DN matches one of
  327. //the given Bases
  328. if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
  329. return false;
  330. }
  331. return $this->dn2ocname($fdn, $ldapName, true);
  332. }
  333. /**
  334. * returns an internal ownCloud name for the given LDAP DN, false on DN outside of search DN
  335. * @param string $dn the dn of the user object
  336. * @param string $ldapName optional, the display name of the object
  337. * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
  338. * @return string|false with with the name to use in ownCloud
  339. */
  340. public function dn2ocname($fdn, $ldapName = null, $isUser = true) {
  341. if($isUser) {
  342. $mapper = $this->getUserMapper();
  343. $nameAttribute = $this->connection->ldapUserDisplayName;
  344. } else {
  345. $mapper = $this->getGroupMapper();
  346. $nameAttribute = $this->connection->ldapGroupDisplayName;
  347. }
  348. //let's try to retrieve the ownCloud name from the mappings table
  349. $ocName = $mapper->getNameByDN($fdn);
  350. if(is_string($ocName)) {
  351. return $ocName;
  352. }
  353. //second try: get the UUID and check if it is known. Then, update the DN and return the name.
  354. $uuid = $this->getUUID($fdn, $isUser);
  355. if(is_string($uuid)) {
  356. $ocName = $mapper->getNameByUUID($uuid);
  357. if(is_string($ocName)) {
  358. $mapper->setDNbyUUID($fdn, $uuid);
  359. return $ocName;
  360. }
  361. } else {
  362. //If the UUID can't be detected something is foul.
  363. \OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', \OCP\Util::INFO);
  364. return false;
  365. }
  366. if(is_null($ldapName)) {
  367. $ldapName = $this->readAttribute($fdn, $nameAttribute);
  368. if(!isset($ldapName[0]) && empty($ldapName[0])) {
  369. \OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.'.', \OCP\Util::INFO);
  370. return false;
  371. }
  372. $ldapName = $ldapName[0];
  373. }
  374. if($isUser) {
  375. $usernameAttribute = $this->connection->ldapExpertUsernameAttr;
  376. if(!empty($usernameAttribute)) {
  377. $username = $this->readAttribute($fdn, $usernameAttribute);
  378. $username = $username[0];
  379. } else {
  380. $username = $uuid;
  381. }
  382. $intName = $this->sanitizeUsername($username);
  383. } else {
  384. $intName = $ldapName;
  385. }
  386. //a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
  387. //disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
  388. //NOTE: mind, disabling cache affects only this instance! Using it
  389. // outside of core user management will still cache the user as non-existing.
  390. $originalTTL = $this->connection->ldapCacheTTL;
  391. $this->connection->setConfiguration(array('ldapCacheTTL' => 0));
  392. if(($isUser && !\OCP\User::userExists($intName))
  393. || (!$isUser && !\OC_Group::groupExists($intName))) {
  394. if($mapper->map($fdn, $intName, $uuid)) {
  395. $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
  396. return $intName;
  397. }
  398. }
  399. $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
  400. $altName = $this->createAltInternalOwnCloudName($intName, $isUser);
  401. if(is_string($altName) && $mapper->map($fdn, $altName, $uuid)) {
  402. return $altName;
  403. }
  404. //if everything else did not help..
  405. \OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', \OCP\Util::INFO);
  406. return false;
  407. }
  408. /**
  409. * gives back the user names as they are used ownClod internally
  410. * @param array $ldapUsers an array with the ldap Users result in style of array ( array ('dn' => foo, 'uid' => bar), ... )
  411. * @return array an array with the user names to use in ownCloud
  412. *
  413. * gives back the user names as they are used ownClod internally
  414. */
  415. public function ownCloudUserNames($ldapUsers) {
  416. return $this->ldap2ownCloudNames($ldapUsers, true);
  417. }
  418. /**
  419. * gives back the group names as they are used ownClod internally
  420. * @param array $ldapGroups an array with the ldap Groups result in style of array ( array ('dn' => foo, 'cn' => bar), ... )
  421. * @return array an array with the group names to use in ownCloud
  422. *
  423. * gives back the group names as they are used ownClod internally
  424. */
  425. public function ownCloudGroupNames($ldapGroups) {
  426. return $this->ldap2ownCloudNames($ldapGroups, false);
  427. }
  428. /**
  429. * @param array $ldapObjects
  430. * @param bool $isUsers
  431. * @return array
  432. */
  433. private function ldap2ownCloudNames($ldapObjects, $isUsers) {
  434. if($isUsers) {
  435. $nameAttribute = $this->connection->ldapUserDisplayName;
  436. } else {
  437. $nameAttribute = $this->connection->ldapGroupDisplayName;
  438. }
  439. $ownCloudNames = array();
  440. foreach($ldapObjects as $ldapObject) {
  441. $nameByLDAP = isset($ldapObject[$nameAttribute]) ? $ldapObject[$nameAttribute] : null;
  442. $ocName = $this->dn2ocname($ldapObject['dn'], $nameByLDAP, $isUsers);
  443. if($ocName) {
  444. $ownCloudNames[] = $ocName;
  445. if($isUsers) {
  446. //cache the user names so it does not need to be retrieved
  447. //again later (e.g. sharing dialogue).
  448. $this->cacheUserExists($ocName);
  449. $this->cacheUserDisplayName($ocName, $nameByLDAP);
  450. }
  451. }
  452. continue;
  453. }
  454. return $ownCloudNames;
  455. }
  456. /**
  457. * caches a user as existing
  458. * @param string $ocName the internal ownCloud username
  459. */
  460. public function cacheUserExists($ocName) {
  461. $this->connection->writeToCache('userExists'.$ocName, true);
  462. }
  463. /**
  464. * caches the user display name
  465. * @param string $ocName the internal ownCloud username
  466. * @param string $displayName the display name
  467. */
  468. public function cacheUserDisplayName($ocName, $displayName) {
  469. $cacheKeyTrunk = 'getDisplayName';
  470. $this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
  471. }
  472. /**
  473. * creates a unique name for internal ownCloud use for users. Don't call it directly.
  474. * @param string $name the display name of the object
  475. * @return string|false with with the name to use in ownCloud or false if unsuccessful
  476. *
  477. * Instead of using this method directly, call
  478. * createAltInternalOwnCloudName($name, true)
  479. */
  480. private function _createAltInternalOwnCloudNameForUsers($name) {
  481. $attempts = 0;
  482. //while loop is just a precaution. If a name is not generated within
  483. //20 attempts, something else is very wrong. Avoids infinite loop.
  484. while($attempts < 20){
  485. $altName = $name . '_' . rand(1000,9999);
  486. if(!\OCP\User::userExists($altName)) {
  487. return $altName;
  488. }
  489. $attempts++;
  490. }
  491. return false;
  492. }
  493. /**
  494. * creates a unique name for internal ownCloud use for groups. Don't call it directly.
  495. * @param string $name the display name of the object
  496. * @return string|false with with the name to use in ownCloud or false if unsuccessful.
  497. *
  498. * Instead of using this method directly, call
  499. * createAltInternalOwnCloudName($name, false)
  500. *
  501. * Group names are also used as display names, so we do a sequential
  502. * numbering, e.g. Developers_42 when there are 41 other groups called
  503. * "Developers"
  504. */
  505. private function _createAltInternalOwnCloudNameForGroups($name) {
  506. $usedNames = $this->groupMapper->getNamesBySearch($name.'_%');
  507. if(!($usedNames) || count($usedNames) === 0) {
  508. $lastNo = 1; //will become name_2
  509. } else {
  510. natsort($usedNames);
  511. $lastName = array_pop($usedNames);
  512. $lastNo = intval(substr($lastName, strrpos($lastName, '_') + 1));
  513. }
  514. $altName = $name.'_'.strval($lastNo+1);
  515. unset($usedNames);
  516. $attempts = 1;
  517. while($attempts < 21){
  518. // Check to be really sure it is unique
  519. // while loop is just a precaution. If a name is not generated within
  520. // 20 attempts, something else is very wrong. Avoids infinite loop.
  521. if(!\OC_Group::groupExists($altName)) {
  522. return $altName;
  523. }
  524. $altName = $name . '_' . ($lastNo + $attempts);
  525. $attempts++;
  526. }
  527. return false;
  528. }
  529. /**
  530. * creates a unique name for internal ownCloud use.
  531. * @param string $name the display name of the object
  532. * @param boolean $isUser whether name should be created for a user (true) or a group (false)
  533. * @return string|false with with the name to use in ownCloud or false if unsuccessful
  534. */
  535. private function createAltInternalOwnCloudName($name, $isUser) {
  536. $originalTTL = $this->connection->ldapCacheTTL;
  537. $this->connection->setConfiguration(array('ldapCacheTTL' => 0));
  538. if($isUser) {
  539. $altName = $this->_createAltInternalOwnCloudNameForUsers($name);
  540. } else {
  541. $altName = $this->_createAltInternalOwnCloudNameForGroups($name);
  542. }
  543. $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
  544. return $altName;
  545. }
  546. /**
  547. * @param string $filter
  548. * @param string|string[] $attr
  549. * @param int $limit
  550. * @param int $offset
  551. * @return array
  552. */
  553. public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null) {
  554. return $this->fetchList($this->searchUsers($filter, $attr, $limit, $offset), (count($attr) > 1));
  555. }
  556. /**
  557. * @param string $filter
  558. * @param string|string[] $attr
  559. * @param int $limit
  560. * @param int $offset
  561. * @return array
  562. */
  563. public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
  564. return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), (count($attr) > 1));
  565. }
  566. /**
  567. * @param array $list
  568. * @param bool $manyAttributes
  569. * @return array
  570. */
  571. private function fetchList($list, $manyAttributes) {
  572. if(is_array($list)) {
  573. if($manyAttributes) {
  574. return $list;
  575. } else {
  576. return array_unique($list, SORT_LOCALE_STRING);
  577. }
  578. }
  579. //error cause actually, maybe throw an exception in future.
  580. return array();
  581. }
  582. /**
  583. * executes an LDAP search, optimized for Users
  584. * @param string $filter the LDAP filter for the search
  585. * @param string|string[] $attr optional, when a certain attribute shall be filtered out
  586. * @param integer $limit
  587. * @param integer $offset
  588. * @return array with the search result
  589. *
  590. * Executes an LDAP search
  591. */
  592. public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
  593. return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
  594. }
  595. /**
  596. * @param string $filter
  597. * @param string|string[] $attr
  598. * @param int $limit
  599. * @param int $offset
  600. * @return false|int
  601. */
  602. public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
  603. return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
  604. }
  605. /**
  606. * executes an LDAP search, optimized for Groups
  607. * @param string $filter the LDAP filter for the search
  608. * @param string|string[] $attr optional, when a certain attribute shall be filtered out
  609. * @param integer $limit
  610. * @param integer $offset
  611. * @return array with the search result
  612. *
  613. * Executes an LDAP search
  614. */
  615. public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
  616. return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
  617. }
  618. /**
  619. * returns the number of available groups
  620. * @param string $filter the LDAP search filter
  621. * @param string[] $attr optional
  622. * @param int|null $limit
  623. * @param int|null $offset
  624. * @return int|bool
  625. */
  626. public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
  627. return $this->count($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
  628. }
  629. /**
  630. * retrieved. Results will according to the order in the array.
  631. * @param int $limit optional, maximum results to be counted
  632. * @param int $offset optional, a starting point
  633. * @return array|false array with the search result as first value and pagedSearchOK as
  634. * second | false if not successful
  635. */
  636. private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
  637. if(!is_null($attr) && !is_array($attr)) {
  638. $attr = array(mb_strtolower($attr, 'UTF-8'));
  639. }
  640. // See if we have a resource, in case not cancel with message
  641. $cr = $this->connection->getConnectionResource();
  642. if(!$this->ldap->isResource($cr)) {
  643. // Seems like we didn't find any resource.
  644. // Return an empty array just like before.
  645. \OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', \OCP\Util::DEBUG);
  646. return false;
  647. }
  648. //check whether paged search should be attempted
  649. $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, intval($limit), $offset);
  650. $linkResources = array_pad(array(), count($base), $cr);
  651. $sr = $this->ldap->search($linkResources, $base, $filter, $attr);
  652. $error = $this->ldap->errno($cr);
  653. if(!is_array($sr) || $error !== 0) {
  654. \OCP\Util::writeLog('user_ldap',
  655. 'Error when searching: '.$this->ldap->error($cr).
  656. ' code '.$this->ldap->errno($cr),
  657. \OCP\Util::ERROR);
  658. \OCP\Util::writeLog('user_ldap', 'Attempt for Paging? '.print_r($pagedSearchOK, true), \OCP\Util::ERROR);
  659. return false;
  660. }
  661. return array($sr, $pagedSearchOK);
  662. }
  663. /**
  664. * processes an LDAP paged search operation
  665. * @param array $sr the array containing the LDAP search resources
  666. * @param string $filter the LDAP filter for the search
  667. * @param array $base an array containing the LDAP subtree(s) that shall be searched
  668. * @param int $iFoundItems number of results in the search operation
  669. * @param int $limit maximum results to be counted
  670. * @param int $offset a starting point
  671. * @param bool $pagedSearchOK whether a paged search has been executed
  672. * @param bool $skipHandling required for paged search when cookies to
  673. * prior results need to be gained
  674. * @return array|false array with the search result as first value and pagedSearchOK as
  675. * second | false if not successful
  676. */
  677. private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
  678. if($pagedSearchOK) {
  679. $cr = $this->connection->getConnectionResource();
  680. foreach($sr as $key => $res) {
  681. $cookie = null;
  682. if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
  683. $this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
  684. }
  685. }
  686. //browsing through prior pages to get the cookie for the new one
  687. if($skipHandling) {
  688. return;
  689. }
  690. // if count is bigger, then the server does not support
  691. // paged search. Instead, he did a normal search. We set a
  692. // flag here, so the callee knows how to deal with it.
  693. if($iFoundItems <= $limit) {
  694. $this->pagedSearchedSuccessful = true;
  695. }
  696. } else {
  697. if(!is_null($limit)) {
  698. \OCP\Util::writeLog('user_ldap', 'Paged search was not available', \OCP\Util::INFO);
  699. }
  700. }
  701. }
  702. /**
  703. * executes an LDAP search, but counts the results only
  704. * @param string $filter the LDAP filter for the search
  705. * @param array $base an array containing the LDAP subtree(s) that shall be searched
  706. * @param string|string[] $attr optional, array, one or more attributes that shall be
  707. * retrieved. Results will according to the order in the array.
  708. * @param int $limit optional, maximum results to be counted
  709. * @param int $offset optional, a starting point
  710. * @param bool $skipHandling indicates whether the pages search operation is
  711. * completed
  712. * @return int|false Integer or false if the search could not be initialized
  713. *
  714. */
  715. private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
  716. \OCP\Util::writeLog('user_ldap', 'Count filter: '.print_r($filter, true), \OCP\Util::DEBUG);
  717. $limitPerPage = intval($this->connection->ldapPagingSize);
  718. if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
  719. $limitPerPage = $limit;
  720. }
  721. $counter = 0;
  722. $count = null;
  723. $this->connection->getConnectionResource();
  724. do {
  725. $continue = false;
  726. $search = $this->executeSearch($filter, $base, $attr,
  727. $limitPerPage, $offset);
  728. if($search === false) {
  729. return $counter > 0 ? $counter : false;
  730. }
  731. list($sr, $pagedSearchOK) = $search;
  732. $count = $this->countEntriesInSearchResults($sr, $limitPerPage, $continue);
  733. $counter += $count;
  734. $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
  735. $offset, $pagedSearchOK, $skipHandling);
  736. $offset += $limitPerPage;
  737. } while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
  738. return $counter;
  739. }
  740. /**
  741. * @param array $searchResults
  742. * @param int $limit
  743. * @param bool $hasHitLimit
  744. * @return int
  745. */
  746. private function countEntriesInSearchResults($searchResults, $limit, &$hasHitLimit) {
  747. $cr = $this->connection->getConnectionResource();
  748. $counter = 0;
  749. foreach($searchResults as $res) {
  750. $count = intval($this->ldap->countEntries($cr, $res));
  751. $counter += $count;
  752. if($count > 0 && $count === $limit) {
  753. $hasHitLimit = true;
  754. }
  755. }
  756. return $counter;
  757. }
  758. /**
  759. * Executes an LDAP search
  760. * @param string $filter the LDAP filter for the search
  761. * @param array $base an array containing the LDAP subtree(s) that shall be searched
  762. * @param string|string[] $attr optional, array, one or more attributes that shall be
  763. * @param int $limit
  764. * @param int $offset
  765. * @param bool $skipHandling
  766. * @return array with the search result
  767. */
  768. private function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
  769. if($limit <= 0) {
  770. //otherwise search will fail
  771. $limit = null;
  772. }
  773. $search = $this->executeSearch($filter, $base, $attr, $limit, $offset);
  774. if($search === false) {
  775. return array();
  776. }
  777. list($sr, $pagedSearchOK) = $search;
  778. $cr = $this->connection->getConnectionResource();
  779. if($skipHandling) {
  780. //i.e. result do not need to be fetched, we just need the cookie
  781. //thus pass 1 or any other value as $iFoundItems because it is not
  782. //used
  783. $this->processPagedSearchStatus($sr, $filter, $base, 1, $limit,
  784. $offset, $pagedSearchOK,
  785. $skipHandling);
  786. return array();
  787. }
  788. // Do the server-side sorting
  789. foreach(array_reverse($attr) as $sortAttr){
  790. foreach($sr as $searchResource) {
  791. $this->ldap->sort($cr, $searchResource, $sortAttr);
  792. }
  793. }
  794. $findings = array();
  795. foreach($sr as $res) {
  796. $findings = array_merge($findings, $this->ldap->getEntries($cr , $res ));
  797. }
  798. $this->processPagedSearchStatus($sr, $filter, $base, $findings['count'],
  799. $limit, $offset, $pagedSearchOK,
  800. $skipHandling);
  801. // if we're here, probably no connection resource is returned.
  802. // to make ownCloud behave nicely, we simply give back an empty array.
  803. if(is_null($findings)) {
  804. return array();
  805. }
  806. if(!is_null($attr)) {
  807. $selection = array();
  808. $multiArray = false;
  809. if(count($attr) > 1) {
  810. $multiArray = true;
  811. $i = 0;
  812. }
  813. foreach($findings as $item) {
  814. if(!is_array($item)) {
  815. continue;
  816. }
  817. $item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
  818. if($multiArray) {
  819. foreach($attr as $key) {
  820. $key = mb_strtolower($key, 'UTF-8');
  821. if(isset($item[$key])) {
  822. if($key !== 'dn') {
  823. $selection[$i][$key] = $this->resemblesDN($key) ?
  824. $this->sanitizeDN($item[$key][0])
  825. : $item[$key][0];
  826. } else {
  827. $selection[$i][$key] = $this->sanitizeDN($item[$key]);
  828. }
  829. }
  830. }
  831. $i++;
  832. } else {
  833. //tribute to case insensitivity
  834. $key = mb_strtolower($attr[0], 'UTF-8');
  835. if(isset($item[$key])) {
  836. if($this->resemblesDN($key)) {
  837. $selection[] = $this->sanitizeDN($item[$key]);
  838. } else {
  839. $selection[] = $item[$key];
  840. }
  841. }
  842. }
  843. }
  844. $findings = $selection;
  845. }
  846. //we slice the findings, when
  847. //a) paged search unsuccessful, though attempted
  848. //b) no paged search, but limit set
  849. if((!$this->getPagedSearchResultState()
  850. && $pagedSearchOK)
  851. || (
  852. !$pagedSearchOK
  853. && !is_null($limit)
  854. )
  855. ) {
  856. $findings = array_slice($findings, intval($offset), $limit);
  857. }
  858. return $findings;
  859. }
  860. /**
  861. * @param string $name
  862. * @return bool|mixed|string
  863. */
  864. public function sanitizeUsername($name) {
  865. if($this->connection->ldapIgnoreNamingRules) {
  866. return $name;
  867. }
  868. // Transliteration
  869. // latin characters to ASCII
  870. $name = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
  871. // Replacements
  872. $name = \OCP\Util::mb_str_replace(' ', '_', $name, 'UTF-8');
  873. // Every remaining disallowed characters will be removed
  874. $name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
  875. return $name;
  876. }
  877. /**
  878. * escapes (user provided) parts for LDAP filter
  879. * @param string $input, the provided value
  880. * @param bool $allowAsterisk whether in * at the beginning should be preserved
  881. * @return string the escaped string
  882. */
  883. public function escapeFilterPart($input, $allowAsterisk = false) {
  884. $asterisk = '';
  885. if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
  886. $asterisk = '*';
  887. $input = mb_substr($input, 1, null, 'UTF-8');
  888. }
  889. $search = array('*', '\\', '(', ')');
  890. $replace = array('\\*', '\\\\', '\\(', '\\)');
  891. return $asterisk . str_replace($search, $replace, $input);
  892. }
  893. /**
  894. * combines the input filters with AND
  895. * @param string[] $filters the filters to connect
  896. * @return string the combined filter
  897. */
  898. public function combineFilterWithAnd($filters) {
  899. return $this->combineFilter($filters, '&');
  900. }
  901. /**
  902. * combines the input filters with OR
  903. * @param string[] $filters the filters to connect
  904. * @return string the combined filter
  905. * Combines Filter arguments with OR
  906. */
  907. public function combineFilterWithOr($filters) {
  908. return $this->combineFilter($filters, '|');
  909. }
  910. /**
  911. * combines the input filters with given operator
  912. * @param string[] $filters the filters to connect
  913. * @param string $operator either & or |
  914. * @return string the combined filter
  915. */
  916. private function combineFilter($filters, $operator) {
  917. $combinedFilter = '('.$operator;
  918. foreach($filters as $filter) {
  919. if(!empty($filter) && $filter[0] !== '(') {
  920. $filter = '('.$filter.')';
  921. }
  922. $combinedFilter.=$filter;
  923. }
  924. $combinedFilter.=')';
  925. return $combinedFilter;
  926. }
  927. /**
  928. * creates a filter part for to perform search for users
  929. * @param string $search the search term
  930. * @return string the final filter part to use in LDAP searches
  931. */
  932. public function getFilterPartForUserSearch($search) {
  933. return $this->getFilterPartForSearch($search,
  934. $this->connection->ldapAttributesForUserSearch,
  935. $this->connection->ldapUserDisplayName);
  936. }
  937. /**
  938. * creates a filter part for to perform search for groups
  939. * @param string $search the search term
  940. * @return string the final filter part to use in LDAP searches
  941. */
  942. public function getFilterPartForGroupSearch($search) {
  943. return $this->getFilterPartForSearch($search,
  944. $this->connection->ldapAttributesForGroupSearch,
  945. $this->connection->ldapGroupDisplayName);
  946. }
  947. /**
  948. * creates a filter part for searches by splitting up the given search
  949. * string into single words
  950. * @param string $search the search term
  951. * @param string[] $searchAttributes needs to have at least two attributes,
  952. * otherwise it does not make sense :)
  953. * @return string the final filter part to use in LDAP searches
  954. * @throws \Exception
  955. */
  956. private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
  957. if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
  958. throw new \Exception('searchAttributes must be an array with at least two string');
  959. }
  960. $searchWords = explode(' ', trim($search));
  961. $wordFilters = array();
  962. foreach($searchWords as $word) {
  963. $word .= '*';
  964. //every word needs to appear at least once
  965. $wordMatchOneAttrFilters = array();
  966. foreach($searchAttributes as $attr) {
  967. $wordMatchOneAttrFilters[] = $attr . '=' . $word;
  968. }
  969. $wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
  970. }
  971. return $this->combineFilterWithAnd($wordFilters);
  972. }
  973. /**
  974. * creates a filter part for searches
  975. * @param string $search the search term
  976. * @param string[]|null $searchAttributes
  977. * @param string $fallbackAttribute a fallback attribute in case the user
  978. * did not define search attributes. Typically the display name attribute.
  979. * @return string the final filter part to use in LDAP searches
  980. */
  981. private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
  982. $filter = array();
  983. $haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
  984. if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
  985. try {
  986. return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
  987. } catch(\Exception $e) {
  988. \OCP\Util::writeLog(
  989. 'user_ldap',
  990. 'Creating advanced filter for search failed, falling back to simple method.',
  991. \OCP\Util::INFO
  992. );
  993. }
  994. }
  995. $search = empty($search) ? '*' : $search.'*';
  996. if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
  997. if(empty($fallbackAttribute)) {
  998. return '';
  999. }
  1000. $filter[] = $fallbackAttribute . '=' . $search;
  1001. } else {
  1002. foreach($searchAttributes as $attribute) {
  1003. $filter[] = $attribute . '=' . $search;
  1004. }
  1005. }
  1006. if(count($filter) === 1) {
  1007. return '('.$filter[0].')';
  1008. }
  1009. return $this->combineFilterWithOr($filter);
  1010. }
  1011. /**
  1012. * returns the filter used for counting users
  1013. * @return string
  1014. */
  1015. public function getFilterForUserCount() {
  1016. $filter = $this->combineFilterWithAnd(array(
  1017. $this->connection->ldapUserFilter,
  1018. $this->connection->ldapUserDisplayName . '=*'
  1019. ));
  1020. return $filter;
  1021. }
  1022. /**
  1023. * @param string $name
  1024. * @param string $password
  1025. * @return bool
  1026. */
  1027. public function areCredentialsValid($name, $password) {
  1028. $name = $this->DNasBaseParameter($name);
  1029. $testConnection = clone $this->connection;
  1030. $credentials = array(
  1031. 'ldapAgentName' => $name,
  1032. 'ldapAgentPassword' => $password
  1033. );
  1034. if(!$testConnection->setConfiguration($credentials)) {
  1035. return false;
  1036. }
  1037. $result=$testConnection->bind();
  1038. $this->connection->bind();
  1039. return $result;
  1040. }
  1041. /**
  1042. * auto-detects the directory's UUID attribute
  1043. * @param string $dn a known DN used to check against
  1044. * @param bool $isUser
  1045. * @param bool $force the detection should be run, even if it is not set to auto
  1046. * @return bool true on success, false otherwise
  1047. */
  1048. private function detectUuidAttribute($dn, $isUser = true, $force = false) {
  1049. if($isUser) {
  1050. $uuidAttr = 'ldapUuidUserAttribute';
  1051. $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
  1052. } else {
  1053. $uuidAttr = 'ldapUuidGroupAttribute';
  1054. $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
  1055. }
  1056. if(($this->connection->$uuidAttr !== 'auto') && !$force) {
  1057. return true;
  1058. }
  1059. if(!empty($uuidOverride) && !$force) {
  1060. $this->connection->$uuidAttr = $uuidOverride;
  1061. return true;
  1062. }
  1063. // for now, supported attributes are entryUUID, nsuniqueid, objectGUID, ipaUniqueID
  1064. $testAttributes = array('entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid');
  1065. foreach($testAttributes as $attribute) {
  1066. $value = $this->readAttribute($dn, $attribute);
  1067. if(is_array($value) && isset($value[0]) && !empty($value[0])) {
  1068. \OCP\Util::writeLog('user_ldap',
  1069. 'Setting '.$attribute.' as '.$uuidAttr,
  1070. \OCP\Util::DEBUG);
  1071. $this->connection->$uuidAttr = $attribute;
  1072. return true;
  1073. }
  1074. }
  1075. \OCP\Util::writeLog('user_ldap',
  1076. 'Could not autodetect the UUID attribute',
  1077. \OCP\Util::ERROR);
  1078. return false;
  1079. }
  1080. /**
  1081. * @param string $dn
  1082. * @param bool $isUser
  1083. * @return string|bool
  1084. */
  1085. public function getUUID($dn, $isUser = true) {
  1086. if($isUser) {
  1087. $uuidAttr = 'ldapUuidUserAttribute';
  1088. $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
  1089. } else {
  1090. $uuidAttr = 'ldapUuidGroupAttribute';
  1091. $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
  1092. }
  1093. $uuid = false;
  1094. if($this->detectUuidAttribute($dn, $isUser)) {
  1095. $uuid = $this->readAttribute($dn, $this->connection->$uuidAttr);
  1096. if( !is_array($uuid)
  1097. && !empty($uuidOverride)
  1098. && $this->detectUuidAttribute($dn, $isUser, true)) {
  1099. $uuid = $this->readAttribute($dn,
  1100. $this->connection->$uuidAttr);
  1101. }
  1102. if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
  1103. $uuid = $uuid[0];
  1104. }
  1105. }
  1106. return $uuid;
  1107. }
  1108. /**
  1109. * converts a binary ObjectGUID into a string representation
  1110. * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
  1111. * @return string
  1112. * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
  1113. */
  1114. private function convertObjectGUID2Str($oguid) {
  1115. $hex_guid = bin2hex($oguid);
  1116. $hex_guid_to_guid_str = '';
  1117. for($k = 1; $k <= 4; ++$k) {
  1118. $hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
  1119. }
  1120. $hex_guid_to_guid_str .= '-';
  1121. for($k = 1; $k <= 2; ++$k) {
  1122. $hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
  1123. }
  1124. $hex_guid_to_guid_str .= '-';
  1125. for($k = 1; $k <= 2; ++$k) {
  1126. $hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
  1127. }
  1128. $hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
  1129. $hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
  1130. return strtoupper($hex_guid_to_guid_str);
  1131. }
  1132. /**
  1133. * gets a SID of the domain of the given dn
  1134. * @param string $dn
  1135. * @return string|bool
  1136. */
  1137. public function getSID($dn) {
  1138. $domainDN = $this->getDomainDNFromDN($dn);
  1139. $cacheKey = 'getSID-'.$domainDN;
  1140. if($this->connection->isCached($cacheKey)) {
  1141. return $this->connection->getFromCache($cacheKey);
  1142. }
  1143. $objectSid = $this->readAttribute($domainDN, 'objectsid');
  1144. if(!is_array($objectSid) || empty($objectSid)) {
  1145. $this->connection->writeToCache($cacheKey, false);
  1146. return false;
  1147. }
  1148. $domainObjectSid = $this->convertSID2Str($objectSid[0]);
  1149. $this->connection->writeToCache($cacheKey, $domainObjectSid);
  1150. return $domainObjectSid;
  1151. }
  1152. /**
  1153. * converts a binary SID into a string representation
  1154. * @param string $sid
  1155. * @return string
  1156. */
  1157. public function convertSID2Str($sid) {
  1158. // The format of a SID binary string is as follows:
  1159. // 1 byte for the revision level
  1160. // 1 byte for the number n of variable sub-ids
  1161. // 6 bytes for identifier authority value
  1162. // n*4 bytes for n sub-ids
  1163. //
  1164. // Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
  1165. // Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
  1166. $revision = ord($sid[0]);
  1167. $numberSubID = ord($sid[1]);
  1168. $subIdStart = 8; // 1 + 1 + 6
  1169. $subIdLength = 4;
  1170. if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
  1171. // Incorrect number of bytes present.
  1172. return '';
  1173. }
  1174. // 6 bytes = 48 bits can be represented using floats without loss of
  1175. // precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
  1176. $iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
  1177. $subIDs = array();
  1178. for ($i = 0; $i < $numberSubID; $i++) {
  1179. $subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
  1180. $subIDs[] = sprintf('%u', $subID[1]);
  1181. }
  1182. // Result for example above: S-1-5-21-249921958-728525901-1594176202
  1183. return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
  1184. }
  1185. /**
  1186. * converts a stored DN so it can be used as base parameter for LDAP queries, internally we store them for usage in LDAP filters
  1187. * @param string $dn the DN
  1188. * @return string
  1189. */
  1190. private function DNasBaseParameter($dn) {
  1191. return str_ireplace('\\5c', '\\', $dn);
  1192. }
  1193. /**
  1194. * checks if the given DN is part of the given base DN(s)
  1195. * @param string $dn the DN
  1196. * @param string[] $bases array containing the allowed base DN or DNs
  1197. * @return bool
  1198. */
  1199. public function isDNPartOfBase($dn, $bases) {
  1200. $belongsToBase = false;
  1201. $bases = $this->sanitizeDN($bases);
  1202. foreach($bases as $base) {
  1203. $belongsToBase = true;
  1204. if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) {
  1205. $belongsToBase = false;
  1206. }
  1207. if($belongsToBase) {
  1208. break;
  1209. }
  1210. }
  1211. return $belongsToBase;
  1212. }
  1213. /**
  1214. * resets a running Paged Search operation
  1215. */
  1216. private function abandonPagedSearch() {
  1217. if($this->connection->hasPagedResultSupport) {
  1218. $cr = $this->connection->getConnectionResource();
  1219. $this->ldap->controlPagedResult($cr, 0, false, $this->lastCookie);
  1220. $this->getPagedSearchResultState();
  1221. $this->lastCookie = '';
  1222. $this->cookies = array();
  1223. }
  1224. }
  1225. /**
  1226. * get a cookie for the next LDAP paged search
  1227. * @param string $base a string with the base DN for the search
  1228. * @param string $filter the search filter to identify the correct search
  1229. * @param int $limit the limit (or 'pageSize'), to identify the correct search well
  1230. * @param int $offset the offset for the new search to identify the correct search really good
  1231. * @return string containing the key or empty if none is cached
  1232. */
  1233. private function getPagedResultCookie($base, $filter, $limit, $offset) {
  1234. if($offset === 0) {
  1235. return '';
  1236. }
  1237. $offset -= $limit;
  1238. //we work with cache here
  1239. $cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . intval($limit) . '-' . intval($offset);
  1240. $cookie = '';
  1241. if(isset($this->cookies[$cacheKey])) {
  1242. $cookie = $this->cookies[$cacheKey];
  1243. if(is_null($cookie)) {
  1244. $cookie = '';
  1245. }
  1246. }
  1247. return $cookie;
  1248. }
  1249. /**
  1250. * set a cookie for LDAP paged search run
  1251. * @param string $base a string with the base DN for the search
  1252. * @param string $filter the search filter to identify the correct search
  1253. * @param int $limit the limit (or 'pageSize'), to identify the correct search well
  1254. * @param int $offset the offset for the run search to identify the correct search really good
  1255. * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response
  1256. * @return void
  1257. */
  1258. private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) {
  1259. // allow '0' for 389ds
  1260. if(!empty($cookie) || $cookie === '0') {
  1261. $cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' .intval($limit) . '-' . intval($offset);
  1262. $this->cookies[$cacheKey] = $cookie;
  1263. $this->lastCookie = $cookie;
  1264. }
  1265. }
  1266. /**
  1267. * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
  1268. * @return boolean|null true on success, null or false otherwise
  1269. */
  1270. public function getPagedSearchResultState() {
  1271. $result = $this->pagedSearchedSuccessful;
  1272. $this->pagedSearchedSuccessful = null;
  1273. return $result;
  1274. }
  1275. /**
  1276. * Prepares a paged search, if possible
  1277. * @param string $filter the LDAP filter for the search
  1278. * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
  1279. * @param string[] $attr optional, when a certain attribute shall be filtered outside
  1280. * @param int $limit
  1281. * @param int $offset
  1282. * @return bool|true
  1283. */
  1284. private function initPagedSearch($filter, $bases, $attr, $limit, $offset) {
  1285. $pagedSearchOK = false;
  1286. if($this->connection->hasPagedResultSupport && ($limit !== 0)) {
  1287. $offset = intval($offset); //can be null
  1288. \OCP\Util::writeLog('user_ldap',
  1289. 'initializing paged search for Filter '.$filter.' base '.print_r($bases, true)
  1290. .' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
  1291. \OCP\Util::DEBUG);
  1292. //get the cookie from the search for the previous search, required by LDAP
  1293. foreach($bases as $base) {
  1294. $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
  1295. if(empty($cookie) && $cookie !== "0" && ($offset > 0)) {
  1296. // no cookie known, although the offset is not 0. Maybe cache run out. We need
  1297. // to start all over *sigh* (btw, Dear Reader, did you know LDAP paged
  1298. // searching was designed by MSFT?)
  1299. // Lukas: No, but thanks to reading that source I finally know!
  1300. // '0' is valid, because 389ds
  1301. $reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
  1302. //a bit recursive, $offset of 0 is the exit
  1303. \OCP\Util::writeLog('user_ldap', 'Looking for cookie L/O '.$limit.'/'.$reOffset, \OCP\Util::INFO);
  1304. $this->search($filter, array($base), $attr, $limit, $reOffset, true);
  1305. $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
  1306. //still no cookie? obviously, the server does not like us. Let's skip paging efforts.
  1307. //TODO: remember this, probably does not change in the next request...
  1308. if(empty($cookie) && $cookie !== '0') {
  1309. // '0' is valid, because 389ds
  1310. $cookie = null;
  1311. }
  1312. }
  1313. if(!is_null($cookie)) {
  1314. //since offset = 0, this is a new search. We abandon other searches that might be ongoing.
  1315. $this->abandonPagedSearch();
  1316. $pagedSearchOK = $this->ldap->controlPagedResult(
  1317. $this->connection->getConnectionResource(), $limit,
  1318. false, $cookie);
  1319. if(!$pagedSearchOK) {
  1320. return false;
  1321. }
  1322. \OCP\Util::writeLog('user_ldap', 'Ready for a paged search', \OCP\Util::DEBUG);
  1323. } else {
  1324. \OCP\Util::writeLog('user_ldap',
  1325. 'No paged search for us, Cpt., Limit '.$limit.' Offset '.$offset,
  1326. \OCP\Util::INFO);
  1327. }
  1328. }
  1329. } else if($this->connection->hasPagedResultSupport && $limit === 0) {
  1330. // a search without limit was requested. However, if we do use
  1331. // Paged Search once, we always must do it. This requires us to
  1332. // initialize it with the configured page size.
  1333. $this->abandonPagedSearch();
  1334. // in case someone set it to 0 … use 500, otherwise no results will
  1335. // be returned.
  1336. $pageSize = intval($this->connection->ldapPagingSize) > 0 ? intval($this->connection->ldapPagingSize) : 500;
  1337. $pagedSearchOK = $this->ldap->controlPagedResult(
  1338. $this->connection->getConnectionResource(), $pageSize, false, ''
  1339. );
  1340. }
  1341. return $pagedSearchOK;
  1342. }
  1343. }