PageRenderTime 131ms CodeModel.GetById 30ms RepoModel.GetById 1ms app.codeStats 0ms

/phpldapadmin-1.2.2/lib/ds_ldap.php

#
PHP | 2401 lines | 1288 code | 488 blank | 625 comment | 432 complexity | 7c4cddc58c3ee37000b35bb156cf6b7f MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. /**
  3. * Classes and functions for communication of Data Stores
  4. *
  5. * @author The phpLDAPadmin development team
  6. * @package phpLDAPadmin
  7. */
  8. /**
  9. * This abstract class provides the basic variables and methods for LDAP datastores
  10. *
  11. * @package phpLDAPadmin
  12. * @subpackage DataStore
  13. */
  14. class ldap extends DS {
  15. # If we fail to connect, set this to true
  16. private $noconnect = false;
  17. # Raw Schema entries
  18. private $_schema_entries = null;
  19. # Schema DN
  20. private $_schemaDN = null;
  21. public function __construct($index) {
  22. if (defined('DEBUG_ENABLED') && DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  23. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  24. $this->index = $index;
  25. $this->type = 'ldap';
  26. # Additional values that can go in our config.php
  27. $this->custom = new StdClass;
  28. $this->default = new StdClass;
  29. /*
  30. * Not used by PLA
  31. # Database Server Variables
  32. $this->default->server['db'] = array(
  33. 'desc'=>'Database Name',
  34. 'untested'=>true,
  35. 'default'=>null);
  36. */
  37. /* This was created for IDS - since it doesnt present STRUCTURAL against objectClasses
  38. * definitions when reading the schema.*/
  39. $this->default->server['schema_oclass_default'] = array(
  40. 'desc'=>'When reading the schema, and it doesnt specify objectClass type, default it to this',
  41. 'default'=>null);
  42. $this->default->server['base'] = array(
  43. 'desc'=>'LDAP Base DNs',
  44. 'default'=>array());
  45. $this->default->server['tls'] = array(
  46. 'desc'=>'Connect using TLS',
  47. 'default'=>false);
  48. # Login Details
  49. $this->default->login['attr'] = array(
  50. 'desc'=>'Attribute to use to find the users DN',
  51. 'default'=>'dn');
  52. $this->default->login['anon_bind'] = array(
  53. 'desc'=>'Enable anonymous bind logins',
  54. 'default'=>true);
  55. $this->default->login['allowed_dns'] = array(
  56. 'desc'=>'Limit logins to users who match any of the following LDAP filters',
  57. 'default'=>array());
  58. $this->default->login['base'] = array(
  59. 'desc'=>'Limit logins to users who are in these base DNs',
  60. 'default'=>array());
  61. $this->default->login['class'] = array(
  62. 'desc'=>'Strict login to users containing a specific objectClasses',
  63. 'default'=>array());
  64. $this->default->proxy['attr'] = array(
  65. 'desc'=>'Attribute to use to find the users DN for proxy based authentication',
  66. 'default'=>array());
  67. # SASL configuration
  68. $this->default->sasl['mech'] = array(
  69. 'desc'=>'SASL mechanism used while binding LDAP server',
  70. 'default'=>'GSSAPI');
  71. $this->default->sasl['realm'] = array(
  72. 'desc'=>'SASL realm name',
  73. 'untested'=>true,
  74. 'default'=>null);
  75. $this->default->sasl['authz_id'] = array(
  76. 'desc'=>'SASL authorization id',
  77. 'untested'=>true,
  78. 'default'=>null);
  79. $this->default->sasl['authz_id_regex'] = array(
  80. 'desc'=>'SASL authorization id PCRE regular expression',
  81. 'untested'=>true,
  82. 'default'=>null);
  83. $this->default->sasl['authz_id_replacement'] = array(
  84. 'desc'=>'SASL authorization id PCRE regular expression replacement string',
  85. 'untested'=>true,
  86. 'default'=>null);
  87. $this->default->sasl['props'] = array(
  88. 'desc'=>'SASL properties',
  89. 'untested'=>true,
  90. 'default'=>null);
  91. }
  92. /**
  93. * Required ABSTRACT functions
  94. */
  95. /**
  96. * Connect and Bind to the Database
  97. *
  98. * @param string Which connection method resource to use
  99. * @return resource|null Connection resource if successful, null if not.
  100. */
  101. protected function connect($method,$debug=false,$new=false) {
  102. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  103. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  104. static $CACHE = array();
  105. $method = $this->getMethod($method);
  106. $bind = array();
  107. if (isset($CACHE[$this->index][$method]) && $CACHE[$this->index][$method])
  108. return $CACHE[$this->index][$method];
  109. # Check if we have logged in and therefore need to use those details as our bind.
  110. $bind['id'] = is_null($this->getLogin($method)) && $method != 'anon' ? $this->getLogin('user') : $this->getLogin($method);
  111. $bind['pass'] = is_null($this->getPassword($method)) && $method != 'anon' ? $this->getPassword('user') : $this->getPassword($method);
  112. # If our bind id is still null, we are not logged in.
  113. if (is_null($bind['id']) && ! in_array($method,array('anon','login')))
  114. return null;
  115. # If we bound to the LDAP server with these details for a different connection, return that resource
  116. if (isset($CACHE[$this->index]) && ! $new)
  117. foreach ($CACHE[$this->index] as $cachedmethod => $resource) {
  118. if (($this->getLogin($cachedmethod) == $bind['id']) && ($this->getPassword($cachedmethod) == $bind['pass'])) {
  119. $CACHE[$this->index][$method] = $resource;
  120. return $CACHE[$this->index][$method];
  121. }
  122. }
  123. $CACHE[$this->index][$method] = null;
  124. # No identifiable connection exists, lets create a new one.
  125. if (DEBUG_ENABLED)
  126. debug_log('Creating NEW connection [%s] for index [%s]',16,0,__FILE__,__LINE__,__METHOD__,
  127. $method,$this->index);
  128. if (function_exists('run_hook'))
  129. run_hook('pre_connect',array('server_id'=>$this->index,'method'=>$method));
  130. if ($this->getValue('server','port'))
  131. $resource = ldap_connect($this->getValue('server','host'),$this->getValue('server','port'));
  132. else
  133. $resource = ldap_connect($this->getValue('server','host'));
  134. $CACHE[$this->index][$method] = $resource;
  135. if (DEBUG_ENABLED)
  136. debug_log('LDAP Resource [%s], Host [%s], Port [%s]',16,0,__FILE__,__LINE__,__METHOD__,
  137. $resource,$this->getValue('server','host'),$this->getValue('server','port'));
  138. if (! is_resource($resource))
  139. debug_dump_backtrace('UNHANDLED, $resource is not a resource',1);
  140. # Go with LDAP version 3 if possible (needed for renaming and Novell schema fetching)
  141. ldap_set_option($resource,LDAP_OPT_PROTOCOL_VERSION,3);
  142. /* Disabling this makes it possible to browse the tree for Active Directory, and seems
  143. * to not affect other LDAP servers (tested with OpenLDAP) as phpLDAPadmin explicitly
  144. * specifies deref behavior for each ldap_search operation. */
  145. ldap_set_option($resource,LDAP_OPT_REFERRALS,0);
  146. # Try to fire up TLS is specified in the config
  147. if ($this->isTLSEnabled())
  148. $this->startTLS($resource);
  149. # If SASL has been configured for binding, then start it now.
  150. if ($this->isSASLEnabled())
  151. $bind['result'] = $this->startSASL($resource,$method);
  152. # Normal bind...
  153. else
  154. $bind['result'] = @ldap_bind($resource,$bind['id'],$bind['pass']);
  155. if ($debug)
  156. debug_dump(array('method'=>$method,'bind'=>$bind,'USER'=>$_SESSION['USER']));
  157. if (DEBUG_ENABLED)
  158. debug_log('Resource [%s], Bind Result [%s]',16,0,__FILE__,__LINE__,__METHOD__,$resource,$bind);
  159. if (! $bind['result']) {
  160. if (DEBUG_ENABLED)
  161. debug_log('Leaving with FALSE, bind FAILed',16,0,__FILE__,__LINE__,__METHOD__);
  162. $this->noconnect = true;
  163. system_message(array(
  164. 'title'=>sprintf('%s %s',_('Unable to connect to LDAP server'),$this->getName()),
  165. 'body'=>sprintf('<b>%s</b>: %s (%s) for <b>%s</b>',_('Error'),$this->getErrorMessage($method),$this->getErrorNum($method),$method),
  166. 'type'=>'error'));
  167. $CACHE[$this->index][$method] = null;
  168. } else {
  169. $this->noconnect = false;
  170. # If this is a proxy session, we need to switch to the proxy user
  171. if ($this->isProxyEnabled() && $bind['id'] && $method != 'anon')
  172. if (! $this->startProxy($resource,$method)) {
  173. $this->noconnect = true;
  174. $CACHE[$this->index][$method] = null;
  175. }
  176. }
  177. if (function_exists('run_hook'))
  178. run_hook('post_connect',array('server_id'=>$this->index,'method'=>$method,'id'=>$bind['id']));
  179. if ($debug)
  180. debug_dump(array($method=>$CACHE[$this->index][$method]));
  181. return $CACHE[$this->index][$method];
  182. }
  183. /**
  184. * Login to the database with the application user/password
  185. *
  186. * @return boolean true|false for successful login.
  187. */
  188. public function login($user=null,$pass=null,$method=null,$new=false) {
  189. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  190. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  191. $userDN = null;
  192. # Get the userDN from the username.
  193. if (! is_null($user)) {
  194. # If login,attr is set to DN, then user should be a DN
  195. if (($this->getValue('login','attr') == 'dn') || $method != 'user')
  196. $userDN = $user;
  197. else
  198. $userDN = $this->getLoginID($user,'login');
  199. if (! $userDN && $this->getValue('login','fallback_dn'))
  200. $userDN = $user;
  201. if (! $userDN)
  202. return false;
  203. } else {
  204. if (in_array($method,array('user','anon'))) {
  205. $method = 'anon';
  206. $userDN = '';
  207. $pass = '';
  208. } else {
  209. $userDN = $this->getLogin('user');
  210. $pass = $this->getPassword('user');
  211. }
  212. }
  213. if (! $this->isAnonBindAllowed() && ! trim($userDN))
  214. return false;
  215. # Temporarily set our user details
  216. $this->setLogin($userDN,$pass,$method);
  217. $connect = $this->connect($method,false,$new);
  218. # If we didnt log in...
  219. if (! is_resource($connect) || $this->noconnect || ! $this->userIsAllowedLogin($userDN)) {
  220. $this->logout($method);
  221. return false;
  222. } else
  223. return true;
  224. }
  225. /**
  226. * Perform a query to the Database
  227. *
  228. * @param string query to perform
  229. * $query['base']
  230. * $query['filter']
  231. * $query['scope']
  232. * $query['attrs'] = array();
  233. * $query['deref']
  234. * @param string Which connection method resource to use
  235. * @param string Index items according to this key
  236. * @param boolean Enable debugging output
  237. * @return array|null Results of query.
  238. */
  239. public function query($query,$method,$index=null,$debug=false) {
  240. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  241. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  242. $attrs_only = 0;
  243. # Defaults
  244. if (! isset($query['attrs']))
  245. $query['attrs'] = array();
  246. else
  247. # Re-index the attrs, PHP throws an error if the keys are not sequential from 0.
  248. $query['attrs'] = array_values($query['attrs']);
  249. if (! isset($query['base'])) {
  250. $bases = $this->getBaseDN();
  251. $query['base'] = array_shift($bases);
  252. }
  253. if (! isset($query['deref']))
  254. $query['deref'] = $_SESSION[APPCONFIG]->getValue('deref','search');
  255. if (! isset($query['filter']))
  256. $query['filter'] = '(&(objectClass=*))';
  257. if (! isset($query['scope']))
  258. $query['scope'] = 'sub';
  259. if (! isset($query['size_limit']))
  260. $query['size_limit'] = 0;
  261. if (! isset($query['time_limit']))
  262. $query['time_limit'] = 0;
  263. if ($query['scope'] == 'base' && ! isset($query['baseok']))
  264. system_message(array(
  265. 'title'=>sprintf('Dont call %s',__METHOD__),
  266. 'body'=>sprintf('Use getDNAttrValues for base queries [%s]',$query['base']),
  267. 'type'=>'info'));
  268. if (is_array($query['base'])) {
  269. system_message(array(
  270. 'title'=>_('Invalid BASE for query'),
  271. 'body'=>_('The query was cancelled because of an invalid base.'),
  272. 'type'=>'error'));
  273. return array();
  274. }
  275. if (DEBUG_ENABLED)
  276. debug_log('%s search PREPARE.',16,0,__FILE__,__LINE__,__METHOD__,$query['scope']);
  277. if ($debug)
  278. debug_dump(array('query'=>$query,'server'=>$this->getIndex(),'con'=>$this->connect($method)));
  279. $resource = $this->connect($method,$debug);
  280. switch ($query['scope']) {
  281. case 'base':
  282. $search = @ldap_read($resource,$query['base'],$query['filter'],$query['attrs'],$attrs_only,$query['size_limit'],$query['time_limit'],$query['deref']);
  283. break;
  284. case 'one':
  285. $search = @ldap_list($resource,$query['base'],$query['filter'],$query['attrs'],$attrs_only,$query['size_limit'],$query['time_limit'],$query['deref']);
  286. break;
  287. case 'sub':
  288. default:
  289. $search = @ldap_search($resource,$query['base'],$query['filter'],$query['attrs'],$attrs_only,$query['size_limit'],$query['time_limit'],$query['deref']);
  290. break;
  291. }
  292. if ($debug)
  293. debug_dump(array('method'=>$method,'search'=>$search,'error'=>$this->getErrorMessage()));
  294. if (DEBUG_ENABLED)
  295. debug_log('Search scope [%s] base [%s] filter [%s] attrs [%s] COMPLETE (%s).',16,0,__FILE__,__LINE__,__METHOD__,
  296. $query['scope'],$query['base'],$query['filter'],$query['attrs'],is_null($search));
  297. if (! $search)
  298. return array();
  299. $return = array();
  300. # Get the first entry identifier
  301. if ($entries = ldap_get_entries($resource,$search)) {
  302. # Remove the count
  303. if (isset($entries['count']))
  304. unset($entries['count']);
  305. # Iterate over the entries
  306. foreach ($entries as $a => $entry) {
  307. if (! isset($entry['dn']))
  308. debug_dump_backtrace('No DN?',1);
  309. # Remove the none entry references.
  310. if (! is_array($entry)) {
  311. unset($entries[$a]);
  312. continue;
  313. }
  314. $dn = $entry['dn'];
  315. unset($entry['dn']);
  316. # Iterate over the attributes
  317. foreach ($entry as $b => $attrs) {
  318. # Remove the none entry references.
  319. if (! is_array($attrs)) {
  320. unset($entry[$b]);
  321. continue;
  322. }
  323. # Remove the count
  324. if (isset($entry[$b]['count']))
  325. unset($entry[$b]['count']);
  326. }
  327. # Our queries always include the DN (the only value not an array).
  328. $entry['dn'] = $dn;
  329. $return[$dn] = $entry;
  330. }
  331. # Sort our results
  332. foreach ($return as $key=> $values)
  333. ksort($return[$key]);
  334. }
  335. if (DEBUG_ENABLED)
  336. debug_log('Returning (%s)',17,0,__FILE__,__LINE__,__METHOD__,$return);
  337. return $return;
  338. }
  339. /**
  340. * Get the last error string
  341. *
  342. * @param string Which connection method resource to use
  343. */
  344. public function getErrorMessage($method=null) {
  345. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  346. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  347. return ldap_error($this->connect($method));
  348. }
  349. /**
  350. * Get the last error number
  351. *
  352. * @param string Which connection method resource to use
  353. */
  354. public function getErrorNum($method=null) {
  355. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  356. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  357. return ldap_errno($this->connect($method));
  358. }
  359. /**
  360. * Additional functions
  361. */
  362. /**
  363. * Get a user ID
  364. *
  365. * @param string Which connection method resource to use
  366. */
  367. public function getLoginID($user,$method=null) {
  368. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  369. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  370. $query['filter'] = sprintf('(&(%s=%s)%s)',
  371. $this->getValue('login','attr'),$user,
  372. $this->getLoginClass() ? sprintf('(objectclass=%s)',join(')(objectclass=',$this->getLoginClass())) : '');
  373. $query['attrs'] = array('dn');
  374. $result = array();
  375. foreach ($this->getLoginBaseDN() as $base) {
  376. $query['base'] = $base;
  377. $result = $this->query($query,$method);
  378. if (count($result) == 1)
  379. break;
  380. }
  381. if (count($result) != 1)
  382. return null;
  383. $detail = array_shift($result);
  384. if (! isset($detail['dn']))
  385. die('ERROR: DN missing?');
  386. else
  387. return $detail['dn'];
  388. }
  389. /**
  390. * Return the login base DNs
  391. * If no login base DNs are defined, then the LDAP server Base DNs are used.
  392. */
  393. private function getLoginBaseDN() {
  394. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  395. debug_log('Entered (%%)',17,1,__FILE__,__LINE__,__METHOD__,$fargs);
  396. if ($this->getValue('login','base'))
  397. return $this->getValue('login','base');
  398. else
  399. return $this->getBaseDN();
  400. }
  401. /**
  402. * Return the login classes that a user must have to login
  403. */
  404. private function getLoginClass() {
  405. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  406. debug_log('Entered (%%)',17,1,__FILE__,__LINE__,__METHOD__,$fargs);
  407. return $this->getValue('login','class');
  408. }
  409. /**
  410. * Return if anonymous bind is allowed in the configuration
  411. */
  412. public function isAnonBindAllowed() {
  413. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  414. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  415. return $this->getValue('login','anon_bind');
  416. }
  417. /**
  418. * Fetches whether TLS has been configured for use with a certain server.
  419. *
  420. * Users may configure phpLDAPadmin to use TLS in config,php thus:
  421. * <code>
  422. * $servers->setValue('server','tls',true|false);
  423. * </code>
  424. *
  425. * @return boolean
  426. */
  427. private function isTLSEnabled() {
  428. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  429. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  430. if ($this->getValue('server','tls') && ! function_exists('ldap_start_tls')) {
  431. error(_('TLS has been enabled in your config, but your PHP install does not support TLS. TLS will be disabled.'),'warn');
  432. return false;
  433. } else
  434. return $this->getValue('server','tls');
  435. }
  436. /**
  437. * If TLS is configured, then start it
  438. */
  439. private function startTLS($resource) {
  440. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  441. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  442. if (! $this->getValue('server','tls') || (function_exists('ldap_start_tls') && ! @ldap_start_tls($resource))) {
  443. system_message(array(
  444. 'title'=>sprintf('%s (%s)',_('Could not start TLS.'),$this->getName()),
  445. 'body'=>sprintf('<b>%s</b>: %s',_('Error'),_('Could not start TLS. Please check your LDAP server configuration.')),
  446. 'type'=>'error'));
  447. return false;
  448. } else
  449. return true;
  450. }
  451. /**
  452. * Fetches whether SASL has been configured for use with a certain server.
  453. *
  454. * Users may configure phpLDAPadmin to use SASL in config,php thus:
  455. * <code>
  456. * $servers->setValue('login','auth_type','sasl');
  457. * </code>
  458. *
  459. * @return boolean
  460. */
  461. private function isSASLEnabled() {
  462. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  463. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  464. if ($this->getValue('login','auth_type') != 'sasl')
  465. return false;
  466. if (! function_exists('ldap_sasl_bind')) {
  467. error(_('SASL has been enabled in your config, but your PHP install does not support SASL. SASL will be disabled.'),'warn');
  468. return false;
  469. }
  470. # If we get here, SASL must be configured.
  471. return true;
  472. }
  473. /**
  474. * If SASL is configured, then start it
  475. * To be able to use SASL, PHP should have been compliled with --with-ldap-sasl=DIR
  476. *
  477. * @todo This has not been tested, please let the developers know if this function works as expected.
  478. */
  479. private function startSASL($resource,$method) {
  480. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  481. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  482. static $CACHE = array();
  483. # We shouldnt be doing SASL binds for anonymous queries?
  484. if ($method == 'anon')
  485. return false;
  486. # At the moment, we have only implemented GSSAPI
  487. if (! in_array(strtolower($this->getValue('sasl','mech')),array('gssapi'))) {
  488. system_message(array(
  489. 'title'=>_('SASL Method not implemented'),
  490. 'body'=>sprintf('<b>%s</b>: %s %s',_('Error'),$this->getValue('sasl','mech'),_('has not been implemented yet')),
  491. 'type'=>'error'));
  492. return false;
  493. }
  494. if (! isset($CACHE['login_dn']))
  495. $CACHE['login_dn'] = is_null($this->getLogin($method)) ? $this->getLogin('user') : $this->getLogin($method);
  496. $CACHE['authz_id'] = '';
  497. /*
  498. # Do we need to rewrite authz_id?
  499. if (! isset($CACHE['authz_id']))
  500. if (! trim($this->getValue('sasl','authz_id')) && strtolower($this->getValue('sasl','mech')) != 'gssapi') {
  501. if (DEBUG_ENABLED)
  502. debug_log('Rewriting bind DN [%s] -> authz_id with regex [%s] and replacement [%s].',9,0,__FILE__,__LINE__,__METHOD__,
  503. $CACHE['login_dn'],
  504. $this->getValue('sasl','authz_id_regex'),
  505. $this->getValue('sasl','authz_id_replacement'));
  506. $CACHE['authz_id'] = @preg_replace($this->getValue('sasl','authz_id_regex'),
  507. $this->getValue('sasl','authz_id_replacement'),$CACHE['login_dn']);
  508. # Invalid regex?
  509. if (is_null($CACHE['authz_id']))
  510. error(sprintf(_('It seems that sasl_authz_id_regex "%s" contains invalid PCRE regular expression. The error is "%s".'),
  511. $this->getValue('sasl','authz_id_regex'),(isset($php_errormsg) ? $php_errormsg : '')),
  512. 'error','index.php');
  513. if (DEBUG_ENABLED)
  514. debug_log('Resource [%s], SASL OPTIONS: mech [%s], realm [%s], authz_id [%s], props [%s]',9,0,__FILE__,__LINE__,__METHOD__,
  515. $resource,
  516. $this->getValue('sasl','mech'),
  517. $this->getValue('sasl','realm'),
  518. $CACHE['authz_id'],
  519. $this->getValue('sasl','props'));
  520. } else
  521. $CACHE['authz_id'] = $this->getValue('sasl','authz_id');
  522. */
  523. # @todo this function is different in PHP5.1 and PHP5.2
  524. return @ldap_sasl_bind($resource,NULL,'',
  525. $this->getValue('sasl','mech'),
  526. $this->getValue('sasl','realm'),
  527. $CACHE['authz_id'],
  528. $this->getValue('sasl','props'));
  529. }
  530. /**
  531. * Fetches whether PROXY AUTH has been configured for use with a certain server.
  532. *
  533. * Users may configure phpLDAPadmin to use PROXY AUTH in config,php thus:
  534. * <code>
  535. * $servers->setValue('login','auth_type','proxy');
  536. * </code>
  537. *
  538. * @return boolean
  539. */
  540. private function isProxyEnabled() {
  541. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  542. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  543. return $this->getValue('login','auth_type') == 'proxy' ? true : false;
  544. }
  545. /**
  546. * If PROXY AUTH is configured, then start it
  547. */
  548. private function startProxy($resource,$method) {
  549. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  550. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  551. $rootdse = $this->getRootDSE();
  552. if (! (isset($rootdse['supportedcontrol']) && in_array('2.16.840.1.113730.3.4.18',$rootdse['supportedcontrol']))) {
  553. system_message(array(
  554. 'title'=>sprintf('%s %s',_('Unable to start proxy connection'),$this->getName()),
  555. 'body'=>sprintf('<b>%s</b>: %s',_('Error'),_('Your LDAP server doesnt seem to support this control')),
  556. 'type'=>'error'));
  557. return false;
  558. }
  559. $filter = '(&';
  560. $dn = '';
  561. $missing = false;
  562. foreach ($this->getValue('proxy','attr') as $attr => $var) {
  563. if (! isset($_SERVER[$var])) {
  564. system_message(array(
  565. 'title'=>sprintf('%s %s',_('Unable to start proxy connection'),$this->getName()),
  566. 'body'=>sprintf('<b>%s</b>: %s (%s)',_('Error'),_('Attribute doesnt exist'),$var),
  567. 'type'=>'error'));
  568. $missing = true;
  569. } else {
  570. if ($attr == 'dn') {
  571. $dn = $var;
  572. break;
  573. } else
  574. $filter .= sprintf('(%s=%s)',$attr,$_SERVER[$var]);
  575. }
  576. }
  577. if ($missing)
  578. return false;
  579. $filter .= ')';
  580. if (! $dn) {
  581. $query['filter'] = $filter;
  582. foreach ($this->getBaseDN() as $base) {
  583. $query['base'] = $base;
  584. if ($search = $this->query($query,$method))
  585. break;
  586. }
  587. if (count($search) != 1) {
  588. system_message(array(
  589. 'title'=>sprintf('%s %s',_('Unable to start proxy connection'),$this->getName()),
  590. 'body'=>sprintf('<b>%s</b>: %s (%s)',_('Error'),_('Search for DN returned the incorrect number of results'),count($search)),
  591. 'type'=>'error'));
  592. return false;
  593. }
  594. $search = array_pop($search);
  595. $dn = $search['dn'];
  596. }
  597. $ctrl = array(
  598. 'oid'=>'2.16.840.1.113730.3.4.18',
  599. 'value'=>sprintf('dn:%s',$dn),
  600. 'iscritical' => true);
  601. if (! ldap_set_option($resource,LDAP_OPT_SERVER_CONTROLS,array($ctrl))) {
  602. system_message(array(
  603. 'title'=>sprintf('%s %s',_('Unable to start proxy connection'),$this->getName()),
  604. 'body'=>sprintf('<b>%s</b>: %s (%s) for <b>%s</b>',_('Error'),$this->getErrorMessage($method),$this->getErrorNum($method),$method),
  605. 'type'=>'error'));
  606. return false;
  607. }
  608. $_SESSION['USER'][$this->index][$method]['proxy'] = blowfish_encrypt($dn);
  609. return true;
  610. }
  611. /**
  612. * Modify attributes of a DN
  613. */
  614. public function modify($dn,$attrs,$method=null) {
  615. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  616. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  617. # We need to supress the error here - programming should detect and report it.
  618. return @ldap_mod_replace($this->connect($method),$dn,$attrs);
  619. }
  620. /**
  621. * Gets the root DN of the specified LDAPServer, or null if it
  622. * can't find it (ie, the server won't give it to us, or it isnt
  623. * specified in the configuration file).
  624. *
  625. * Tested with OpenLDAP 2.0, Netscape iPlanet, and Novell eDirectory 8.7 (nldap.com)
  626. * Please report any and all bugs!!
  627. *
  628. * Please note: On FC systems, it seems that php_ldap uses /etc/openldap/ldap.conf in
  629. * the search base if it is blank - so edit that file and comment out the BASE line.
  630. *
  631. * @param string Which connection method resource to use
  632. * @return array dn|null The root DN of the server on success (string) or null on error.
  633. * @todo Sort the entries, so that they are in the correct DN order.
  634. */
  635. public function getBaseDN($method=null) {
  636. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  637. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  638. static $CACHE;
  639. $method = $this->getMethod($method);
  640. $result = array();
  641. if (isset($CACHE[$this->index][$method]))
  642. return $CACHE[$this->index][$method];
  643. # If the base is set in the configuration file, then just return that.
  644. if (count($this->getValue('server','base'))) {
  645. if (DEBUG_ENABLED)
  646. debug_log('Return BaseDN from Config [%s]',17,0,__FILE__,__LINE__,__METHOD__,implode('|',$this->getValue('server','base')));
  647. $CACHE[$this->index][$method] = $this->getValue('server','base');
  648. # We need to figure it out.
  649. } else {
  650. if (DEBUG_ENABLED)
  651. debug_log('Connect to LDAP to find BaseDN',80,0,__FILE__,__LINE__,__METHOD__);
  652. # Set this to empty, in case we loop back here looking for the baseDNs
  653. $CACHE[$this->index][$method] = array();
  654. $results = $this->getDNAttrValues('',$method);
  655. if (isset($results['namingcontexts'])) {
  656. if (DEBUG_ENABLED)
  657. debug_log('LDAP Entries:%s',80,0,__FILE__,__LINE__,__METHOD__,implode('|',$results['namingcontexts']));
  658. $result = $results['namingcontexts'];
  659. }
  660. $CACHE[$this->index][$method] = $result;
  661. }
  662. return $CACHE[$this->index][$method];
  663. }
  664. /**
  665. * Gets whether an entry exists based on its DN. If the entry exists,
  666. * returns true. Otherwise returns false.
  667. *
  668. * @param string The DN of the entry of interest.
  669. * @param string Which connection method resource to use
  670. * @return boolean
  671. */
  672. public function dnExists($dn,$method=null) {
  673. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  674. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  675. $results = $this->getDNAttrValues($dn,$method);
  676. if ($results)
  677. return $results;
  678. else
  679. return false;
  680. }
  681. /**
  682. * Given a DN string, this returns the top container portion of the string.
  683. *
  684. * @param string The DN whose container string to return.
  685. * @return string The container
  686. */
  687. public function getContainerTop($dn) {
  688. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  689. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  690. $return = $dn;
  691. foreach ($this->getBaseDN() as $base) {
  692. if (preg_match("/${base}$/i",$dn)) {
  693. $return = $base;
  694. break;
  695. }
  696. }
  697. if (DEBUG_ENABLED)
  698. debug_log('Returning (%s)',17,0,__FILE__,__LINE__,__METHOD__,$return);
  699. return $return;
  700. }
  701. /**
  702. * Given a DN string and a path like syntax, this returns the parent container portion of the string.
  703. *
  704. * @param string The DN whose container string to return.
  705. * @param string Either '/', '.' or something like '../../<rdn>'
  706. * @return string The container
  707. */
  708. public function getContainerPath($dn,$path='..') {
  709. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  710. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  711. $top = $this->getContainerTop($dn);
  712. if ($path[0] == '/') {
  713. $dn = $top;
  714. $path = substr($path,1);
  715. } elseif ($path == '.') {
  716. return $dn;
  717. }
  718. $parenttree = explode('/',$path);
  719. foreach ($parenttree as $key => $value) {
  720. if ($value == '..') {
  721. if ($this->getContainer($dn))
  722. $dn = $this->getContainer($dn);
  723. if ($dn == $top)
  724. break;
  725. } elseif($value)
  726. $dn = sprintf('%s,%s',$value,$dn);
  727. else
  728. break;
  729. }
  730. if (! $dn) {
  731. debug_dump(array(__METHOD__,'dn'=>$dn,'path'=>$path));
  732. debug_dump_backtrace('Container is empty?',1);
  733. }
  734. return $dn;
  735. }
  736. /**
  737. * Given a DN string, this returns the parent container portion of the string.
  738. * For example. given 'cn=Manager,dc=example,dc=com', this function returns
  739. * 'dc=example,dc=com'.
  740. *
  741. * @param string The DN whose container string to return.
  742. * @return string The container
  743. */
  744. public function getContainer($dn) {
  745. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  746. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  747. $parts = $this->explodeDN($dn);
  748. if (count($parts) <= 1)
  749. $return = null;
  750. else {
  751. $return = $parts[1];
  752. for ($i=2;$i<count($parts);$i++)
  753. $return .= sprintf(',%s',$parts[$i]);
  754. }
  755. if (DEBUG_ENABLED)
  756. debug_log('Returning (%s)',17,0,__FILE__,__LINE__,__METHOD__,$return);
  757. return $return;
  758. }
  759. /**
  760. * Gets a list of child entries for an entry. Given a DN, this function fetches the list of DNs of
  761. * child entries one level beneath the parent. For example, for the following tree:
  762. *
  763. * <code>
  764. * dc=example,dc=com
  765. * ou=People
  766. * cn=Dave
  767. * cn=Fred
  768. * cn=Joe
  769. * ou=More People
  770. * cn=Mark
  771. * cn=Bob
  772. * </code>
  773. *
  774. * Calling <code>getContainerContents("ou=people,dc=example,dc=com")</code>
  775. * would return the following list:
  776. *
  777. * <code>
  778. * cn=Dave
  779. * cn=Fred
  780. * cn=Joe
  781. * ou=More People
  782. * </code>
  783. *
  784. * @param string The DN of the entry whose children to return.
  785. * @param string Which connection method resource to use
  786. * @param int (optional) The maximum number of entries to return.
  787. * If unspecified, no limit is applied to the number of entries in the returned.
  788. * @param string (optional) An LDAP filter to apply when fetching children, example: "(objectClass=inetOrgPerson)"
  789. * @param constant (optional) The LDAP deref setting to use in the query
  790. * @return array An array of DN strings listing the immediate children of the specified entry.
  791. */
  792. public function getContainerContents($dn,$method=null,$size_limit=0,$filter='(objectClass=*)',$deref=LDAP_DEREF_NEVER) {
  793. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  794. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  795. $return = array();
  796. $query = array();
  797. $query['base'] = $this->escapeDN($dn);
  798. $query['attrs'] = array('dn');
  799. $query['filter'] = $filter;
  800. $query['deref'] = $deref;
  801. $query['scope'] = 'one';
  802. $query['size_limit'] = $size_limit;
  803. $results = $this->query($query,$method);
  804. if ($results) {
  805. foreach ($results as $index => $entry) {
  806. $child_dn = $entry['dn'];
  807. array_push($return,$child_dn);
  808. }
  809. }
  810. if (DEBUG_ENABLED)
  811. debug_log('Returning (%s)',17,0,__FILE__,__LINE__,__METHOD__,$return);
  812. # Sort the results
  813. asort($return);
  814. return $return;
  815. }
  816. /**
  817. * Explode a DN into an array of its RDN parts.
  818. *
  819. * @param string The DN to explode.
  820. * @param int (optional) Whether to include attribute names (see http://php.net/ldap_explode_dn for details)
  821. *
  822. * @return array An array of RDN parts of this format:
  823. * <code>
  824. * Array
  825. * (
  826. * [0] => uid=ppratt
  827. * [1] => ou=People
  828. * [2] => dc=example
  829. * [3] => dc=com
  830. * )
  831. * </code>
  832. *
  833. * NOTE: When a multivalue RDN is passed to ldap_explode_dn, the results returns with 'value + value';
  834. */
  835. private function explodeDN($dn,$with_attributes=0) {
  836. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  837. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  838. static $CACHE;
  839. if (isset($CACHE['explode'][$dn][$with_attributes])) {
  840. if (DEBUG_ENABLED)
  841. debug_log('Return CACHED result (%s) for (%s)',1,0,__FILE__,__LINE__,__METHOD__,
  842. $CACHE['explode'][$dn][$with_attributes],$dn);
  843. return $CACHE['explode'][$dn][$with_attributes];
  844. }
  845. $dn = addcslashes($dn,'<>+";');
  846. # split the dn
  847. $result[0] = ldap_explode_dn($this->escapeDN($dn),0);
  848. $result[1] = ldap_explode_dn($this->escapeDN($dn),1);
  849. if (! $result[$with_attributes]) {
  850. if (DEBUG_ENABLED)
  851. debug_log('Returning NULL - NO result.',1,0,__FILE__,__LINE__,__METHOD__);
  852. return array();
  853. }
  854. # Remove our count value that ldap_explode_dn returns us.
  855. unset($result[0]['count']);
  856. unset($result[1]['count']);
  857. # Record the forward and reverse entries in the cache.
  858. foreach ($result as $key => $value) {
  859. # translate hex code into ascii for display
  860. $result[$key] = $this->unescapeDN($value);
  861. $CACHE['explode'][implode(',',$result[0])][$key] = $result[$key];
  862. $CACHE['explode'][implode(',',array_reverse($result[0]))][$key] = array_reverse($result[$key]);
  863. }
  864. if (DEBUG_ENABLED)
  865. debug_log('Returning (%s)',17,0,__FILE__,__LINE__,__METHOD__,$result[$with_attributes]);
  866. return $result[$with_attributes];
  867. }
  868. /**
  869. * Parse a DN and escape any special characters
  870. */
  871. protected function escapeDN($dn) {
  872. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  873. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  874. if (! trim($dn))
  875. return $dn;
  876. # Check if the RDN has a comma and escape it.
  877. while (preg_match('/([^\\\\]),(\s*[^=]*\s*),/',$dn))
  878. $dn = preg_replace('/([^\\\\]),(\s*[^=]*\s*),/','$1\\\\2C$2,',$dn);
  879. $dn = preg_replace('/([^\\\\]),(\s*[^=]*\s*)([^,])$/','$1\\\\2C$2$3',$dn);
  880. if (DEBUG_ENABLED)
  881. debug_log('Returning (%s)',17,0,__FILE__,__LINE__,__METHOD__,$dn);
  882. return $dn;
  883. }
  884. /**
  885. * Parse a DN and unescape any special characters
  886. */
  887. private function unescapeDN($dn) {
  888. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  889. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  890. if (is_array($dn)) {
  891. $a = array();
  892. foreach ($dn as $key => $rdn)
  893. $a[$key] = preg_replace('/\\\([0-9A-Fa-f]{2})/e',"''.chr(hexdec('\\1')).''",$rdn);
  894. return $a;
  895. } else
  896. return preg_replace('/\\\([0-9A-Fa-f]{2})/e',"''.chr(hexdec('\\1')).''",$dn);
  897. }
  898. public function getRootDSE($method=null) {
  899. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  900. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  901. $query = array();
  902. $query['base'] = '';
  903. $query['scope'] = 'base';
  904. $query['attrs'] = $this->getValue('server','root_dse_attributes');
  905. $query['baseok'] = true;
  906. $results = $this->query($query,$method);
  907. if (is_array($results) && count($results) == 1)
  908. return array_change_key_case(array_pop($results));
  909. else
  910. return array();
  911. }
  912. /** Schema Methods **/
  913. /**
  914. * This function will query the ldap server and request the subSchemaSubEntry which should be the Schema DN.
  915. *
  916. * If we cant connect to the LDAP server, we'll return false.
  917. * If we can connect but cant get the entry, then we'll return null.
  918. *
  919. * @param string Which connection method resource to use
  920. * @param dn The DN to use to obtain the schema
  921. * @return array|false Schema if available, null if its not or false if we cant connect.
  922. */
  923. private function getSchemaDN($method=null,$dn='') {
  924. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  925. debug_log('Entered (%%)',25,0,__FILE__,__LINE__,__METHOD__,$fargs);
  926. # If we already got the SchemaDN, then return it.
  927. if ($this->_schemaDN)
  928. return $this->_schemaDN;
  929. if (! $this->connect($method))
  930. return false;
  931. $search = @ldap_read($this->connect($method),$dn,'objectclass=*',array('subschemaSubentry'),false,0,10,LDAP_DEREF_NEVER);
  932. if (DEBUG_ENABLED)
  933. debug_log('Search returned (%s)',24,0,__FILE__,__LINE__,__METHOD__,is_resource($search));
  934. # Fix for broken ldap.conf configuration.
  935. if (! $search && ! $dn) {
  936. if (DEBUG_ENABLED)
  937. debug_log('Trying to find the DN for "broken" ldap.conf',80,0,__FILE__,__LINE__,__METHOD__);
  938. if (isset($this->_baseDN)) {
  939. foreach ($this->_baseDN as $base) {
  940. $search = @ldap_read($this->connect($method),$base,'objectclass=*',array('subschemaSubentry'),false,0,10,LDAP_DEREF_NEVER);
  941. if (DEBUG_ENABLED)
  942. debug_log('Search returned (%s) for base (%s)',24,0,__FILE__,__LINE__,__METHOD__,
  943. is_resource($search),$base);
  944. if ($search)
  945. break;
  946. }
  947. }
  948. }
  949. if (! $search)
  950. return null;
  951. if (! @ldap_count_entries($this->connect($method),$search)) {
  952. if (DEBUG_ENABLED)
  953. debug_log('Search returned 0 entries. Returning NULL',25,0,__FILE__,__LINE__,__METHOD__);
  954. return null;
  955. }
  956. $entries = @ldap_get_entries($this->connect($method),$search);
  957. if (DEBUG_ENABLED)
  958. debug_log('Search returned [%s]',24,0,__FILE__,__LINE__,__METHOD__,$entries);
  959. if (! $entries || ! is_array($entries))
  960. return null;
  961. $entry = isset($entries[0]) ? $entries[0] : false;
  962. if (! $entry) {
  963. if (DEBUG_ENABLED)
  964. debug_log('Entry is false, Returning NULL',80,0,__FILE__,__LINE__,__METHOD__);
  965. return null;
  966. }
  967. $sub_schema_sub_entry = isset($entry[0]) ? $entry[0] : false;
  968. if (! $sub_schema_sub_entry) {
  969. if (DEBUG_ENABLED)
  970. debug_log('Sub Entry is false, Returning NULL',80,0,__FILE__,__LINE__,__METHOD__);
  971. return null;
  972. }
  973. $this->_schemaDN = isset($entry[$sub_schema_sub_entry][0]) ? $entry[$sub_schema_sub_entry][0] : false;
  974. if (DEBUG_ENABLED)
  975. debug_log('Returning (%s)',25,0,__FILE__,__LINE__,__METHOD__,$this->_schemaDN);
  976. return $this->_schemaDN;
  977. }
  978. /**
  979. * Fetches the raw schema array for the subschemaSubentry of the server. Note,
  980. * this function has grown many hairs to accomodate more LDAP servers. It is
  981. * needfully complicated as it now supports many popular LDAP servers that
  982. * don't necessarily expose their schema "the right way".
  983. *
  984. * Please note: On FC systems, it seems that php_ldap uses /etc/openldap/ldap.conf in
  985. * the search base if it is blank - so edit that file and comment out the BASE line.
  986. *
  987. * @param string Which connection method resource to use
  988. * @param string A string indicating which type of schema to
  989. * fetch. Five valid values: 'objectclasses', 'attributetypes',
  990. * 'ldapsyntaxes', 'matchingruleuse', or 'matchingrules'.
  991. * Case insensitive.
  992. * @param dn (optional) This paremeter is the DN of the entry whose schema you
  993. * would like to fetch. Entries have the option of specifying
  994. * their own subschemaSubentry that points to the DN of the system
  995. * schema entry which applies to this attribute. If unspecified,
  996. * this will try to retrieve the schema from the RootDSE subschemaSubentry.
  997. * Failing that, we use some commonly known schema DNs. Default
  998. * value is the Root DSE DN (zero-length string)
  999. * @return array an array of strings of this form:
  1000. * Array (
  1001. * [0] => "(1.3.6.1.4.1.7165.1.2.2.4 NAME 'gidPool' DESC 'Pool ...
  1002. * [1] => "(1.3.6.1.4.1.7165.2.2.3 NAME 'sambaAccount' DESC 'Sa ...
  1003. * etc.
  1004. */
  1005. private function getRawSchema($method,$schema_to_fetch,$dn='') {
  1006. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1007. debug_log('Entered (%%)',25,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1008. $valid_schema_to_fetch = array('objectclasses','attributetypes','ldapsyntaxes','matchingrules','matchingruleuse');
  1009. if (! $this->connect($method) || $this->noconnect)
  1010. return false;
  1011. # error checking
  1012. $schema_to_fetch = strtolower($schema_to_fetch);
  1013. if (! is_null($this->_schema_entries) && isset($this->_schema_entries[$schema_to_fetch])) {
  1014. $schema = $this->_schema_entries[$schema_to_fetch];
  1015. if (DEBUG_ENABLED)
  1016. debug_log('Returning CACHED (%s)',25,0,__FILE__,__LINE__,__METHOD__,$schema);
  1017. return $schema;
  1018. }
  1019. # This error message is not localized as only developers should ever see it
  1020. if (! in_array($schema_to_fetch,$valid_schema_to_fetch))
  1021. error(sprintf('Bad parameter provided to function to %s::getRawSchema(). "%s" is not valid for the schema_to_fetch parameter.',
  1022. get_class($this),$schema_to_fetch),'error','index.php');
  1023. # Try to get the schema DN from the specified entry.
  1024. $schema_dn = $this->getSchemaDN($method,$dn);
  1025. # Do we need to try again with the Root DSE?
  1026. if (! $schema_dn && trim($dn))
  1027. $schema_dn = $this->getSchemaDN($method,'');
  1028. # Store the eventual schema retrieval in $schema_search
  1029. $schema_search = null;
  1030. if ($schema_dn) {
  1031. if (DEBUG_ENABLED)
  1032. debug_log('Using Schema DN (%s)',24,0,__FILE__,__LINE__,__METHOD__,$schema_dn);
  1033. foreach (array('(objectClass=*)','(objectClass=subschema)') as $schema_filter) {
  1034. if (DEBUG_ENABLED)
  1035. debug_log('Looking for schema with Filter (%s)',24,0,__FILE__,__LINE__,__METHOD__,$schema_filter);
  1036. $schema_search = @ldap_read($this->connect($method),$schema_dn,$schema_filter,array($schema_to_fetch),false,0,10,LDAP_DEREF_NEVER);
  1037. if (is_null($schema_search))
  1038. continue;
  1039. $schema_entries = @ldap_get_entries($this->connect($method),$schema_search);
  1040. if (DEBUG_ENABLED)
  1041. debug_log('Search returned [%s]',24,0,__FILE__,__LINE__,__METHOD__,$schema_entries);
  1042. if (is_array($schema_entries) && isset($schema_entries['count']) && $schema_entries['count']) {
  1043. if (DEBUG_ENABLED)
  1044. debug_log('Found schema with (DN:%s) (FILTER:%s) (ATTR:%s)',24,0,__FILE__,__LINE__,__METHOD__,
  1045. $schema_dn,$schema_filter,$schema_to_fetch);
  1046. break;
  1047. }
  1048. if (DEBUG_ENABLED)
  1049. debug_log('Didnt find schema with filter (%s)',24,0,__FILE__,__LINE__,__METHOD__,$schema_filter);
  1050. unset($schema_entries);
  1051. $schema_search = null;
  1052. }
  1053. }
  1054. /* Second chance: If the DN or Root DSE didn't give us the subschemaSubentry, ie $schema_search
  1055. * is still null, use some common subSchemaSubentry DNs as a work-around. */
  1056. if (is_null($schema_search)) {
  1057. if (DEBUG_ENABLED)
  1058. debug_log('Attempting work-arounds for "broken" LDAP servers...',24,0,__FILE__,__LINE__,__METHOD__);
  1059. foreach ($this->getBaseDN() as $base) {
  1060. $ldap['W2K3 AD'][expand_dn_with_base($base,'cn=Aggregate,cn=Schema,cn=configuration,')] = '(objectClass=*)';
  1061. $ldap['W2K AD'][expand_dn_with_base($base,'cn=Schema,cn=configuration,')] = '(objectClass=*)';
  1062. $ldap['W2K AD'][expand_dn_with_base($base,'cn=Schema,ou=Admin,')] = '(objectClass=*)';
  1063. }
  1064. # OpenLDAP and Novell
  1065. $ldap['OpenLDAP']['cn=subschema'] = '(objectClass=*)';
  1066. foreach ($ldap as $ldap_server_name => $ldap_options) {
  1067. foreach ($ldap_options as $ldap_dn => $ldap_filter) {
  1068. if (DEBUG_ENABLED)
  1069. debug_log('Attempting [%s] (%s) (%s)<BR>',24,0,__FILE__,__LINE__,__METHOD__,
  1070. $ldap_server_name,$ldap_dn,$ldap_filter);
  1071. $schema_search = @ldap_read($this->connect($method),$ldap_dn,$ldap_filter,array($schema_to_fetch),false,0,10,LDAP_DEREF_NEVER);
  1072. if (is_null($schema_search))
  1073. continue;
  1074. $schema_entries = @ldap_get_entries($this->connect($method),$schema_search);
  1075. if (DEBUG_ENABLED)
  1076. debug_log('Search returned [%s]',24,0,__FILE__,__LINE__,__METHOD__,$schema_entries);
  1077. if ($schema_entries && isset($schema_entries[0][$schema_to_fetch])) {
  1078. if (DEBUG_ENABLED)
  1079. debug_log('Found schema with filter of (%s)',24,0,__FILE__,__LINE__,__METHOD__,$ldap_filter);
  1080. break;
  1081. }
  1082. if (DEBUG_ENABLED)
  1083. debug_log('Didnt find schema with filter (%s)',24,0,__FILE__,__LINE__,__METHOD__,$ldap_filter);
  1084. unset($schema_entries);
  1085. $schema_search = null;
  1086. }
  1087. if ($schema_search)
  1088. break;
  1089. }
  1090. }
  1091. # Option 3: try cn=config
  1092. $olc_schema = 'olc'.$schema_to_fetch;
  1093. $olc_schema_found = false;
  1094. if (is_null($schema_search)) {
  1095. if (DEBUG_ENABLED)
  1096. debug_log('Attempting cn=config work-around...',24,0,__FILE__,__LINE__,__METHOD__);
  1097. $ldap_dn = 'cn=schema,cn=config';
  1098. $ldap_filter = '(objectClass=*)';
  1099. $schema_search = @ldap_search($this->connect($method),$ldap_dn,$ldap_filter,array($olc_schema),false,0,10,LDAP_DEREF_NEVER);
  1100. if (! is_null($schema_search)) {
  1101. $schema_entries = @ldap_get_entries($this->connect($method),$schema_search);
  1102. if (DEBUG_ENABLED)
  1103. debug_log('Search returned [%s]',24,0,__FILE__,__LINE__,__METHOD__,$schema_entries);
  1104. if ($schema_entries) {
  1105. if (DEBUG_ENABLED)
  1106. debug_log('Found schema with filter of (%s) and attribute filter (%s)',24,0,__FILE__,__LINE__,__METHOD__,$ldap_filter,$olc_schema);
  1107. $olc_schema_found = true;
  1108. } else {
  1109. if (DEBUG_ENABLED)
  1110. debug_log('Didnt find schema with filter (%s) and attribute filter (%s)',24,0,__FILE__,__LINE__,__METHOD__,$ldap_filter,$olc_schema);
  1111. unset($schema_entries);
  1112. $schema_search = null;
  1113. }
  1114. }
  1115. }
  1116. if (is_null($schema_search)) {
  1117. /* Still cant find the schema, try with the RootDSE
  1118. * Attempt to pull schema from Root DSE with scope "base", or
  1119. * Attempt to pull schema from Root DSE with scope "one" (work-around for Isode M-Vault X.500/LDAP) */
  1120. foreach (array('base','one') as $ldap_scope) {
  1121. if (DEBUG_ENABLED)
  1122. debug_log('Attempting to find schema with scope (%s), filter (objectClass=*) and a blank base.',24,0,__FILE__,__LINE__,__METHOD__,
  1123. $ldap_scope);
  1124. switch ($ldap_scope) {
  1125. case 'base':
  1126. $schema_search = @ldap_read($this->connect($method),'','(objectClass=*)',array($schema_to_fetch),false,0,10,LDAP_DEREF_NEVER);
  1127. break;
  1128. case 'one':
  1129. $schema_search = @ldap_list($this->connect($method),'','(objectClass=*)',array($schema_to_fetch),false,0,10,LDAP_DEREF_NEVER);
  1130. break;
  1131. }
  1132. if (is_null($schema_search))
  1133. continue;
  1134. $schema_entries = @ldap_get_entries($this->connect($method),$schema_search);
  1135. if (DEBUG_ENABLED)
  1136. debug_log('Search returned [%s]',24,0,__FILE__,__LINE__,__METHOD__,$schema_entries);
  1137. if ($schema_entries && isset($schema_entries[0][$schema_to_fetch])) {
  1138. if (DEBUG_ENABLED)
  1139. debug_log('Found schema with filter of (%s)',24,0,__FILE__,__LINE__,__METHOD__,'(objectClass=*)');
  1140. break;
  1141. }
  1142. if (DEBUG_ENABLED)
  1143. debug_log('Didnt find schema with filter (%s)',24,0,__FILE__,__LINE__,__METHOD__,'(objectClass=*)');
  1144. unset($schema_entries);
  1145. $schema_search = null;
  1146. }
  1147. }
  1148. $schema_error_message = 'Please contact the phpLDAPadmin developers and let them know:<ul><li>Which LDAP server you are running, including which version<li>What OS it is running on<li>Which version of PHP<li>As well as a link to some documentation that describes how to obtain the SCHEMA information</ul><br />We\'ll then add support for your LDAP server in an upcoming release.';
  1149. $schema_error_message_array = array('objectclasses','attributetypes');
  1150. # Shall we just give up?
  1151. if (is_null($schema_search)) {
  1152. # We need to have objectclasses and attribues, so display an error, asking the user to get us this information.
  1153. if (in_array($schema_to_fetch,$schema_error_message_array))
  1154. system_message(array(
  1155. 'title'=>sprintf('%s (%s)',_('Our attempts to find your SCHEMA have failed'),$schema_to_fetch),
  1156. 'body'=>sprintf('<b>%s</b>: %s',_('Error'),$schema_error_message),
  1157. 'type'=>'error'));
  1158. else
  1159. if (DEBUG_ENABLED)
  1160. debug_log('Returning because schema_search is NULL ()',25,0,__FILE__,__LINE__,__METHOD__);
  1161. # We'll set this, so if we return here our cache will return the known false.
  1162. $this->_schema_entries[$schema_to_fetch] = false;
  1163. return false;
  1164. }
  1165. if (! $schema_entries) {
  1166. $return = false;
  1167. if (DEBUG_ENABLED)
  1168. debug_log('Returning false since ldap_get_entries() returned false.',25,0,__FILE__,__LINE__,__METHOD__,$return);
  1169. return $return;
  1170. }
  1171. if ($olc_schema_found) {
  1172. unset ($schema_entries['count']);
  1173. foreach ($schema_entries as $entry) {
  1174. if (isset($entry[$olc_schema])) {
  1175. unset($entry[$olc_schema]['count']);
  1176. foreach ($entry[$olc_schema] as $schema_definition)
  1177. /* Schema definitions in child nodes prefix the schema entries with "{n}"
  1178. the preg_replace call strips out this prefix. */
  1179. $schema[] = preg_replace('/^\{\d*\}\(/','(',$schema_definition);
  1180. }
  1181. }
  1182. if (isset($schema)) {
  1183. $this->_schema_entries[$olc_schema] = $schema;
  1184. if (DEBUG_ENABLED)
  1185. debug_log('Returning (%s)',25,0,__FILE__,__LINE__,__METHOD__,$schema);
  1186. return $schema;
  1187. } else
  1188. return null;
  1189. }
  1190. if (! isset($schema_entries[0][$schema_to_fetch])) {
  1191. if (in_array($schema_to_fetch,$schema_error_message_array)) {
  1192. error(sprintf('Our attempts to find your SCHEMA for "%s" have return UNEXPECTED results.<br /><br /><small>(We expected a "%s" in the $schema array but it wasnt there.)</small><br /><br />%s<br /><br />Dump of $schema_search:<hr /><pre><small>%s</small></pre>',
  1193. $schema_to_fetch,gettype($schema_search),$schema_error_message,serialize($schema_entries)),'error','index.php');
  1194. } else {
  1195. $return = false;
  1196. if (DEBUG_ENABLED)
  1197. debug_log('Returning because (%s) isnt in the schema array. (%s)',25,0,__FILE__,__LINE__,__METHOD__,$schema_to_fetch,$return);
  1198. return $return;
  1199. }
  1200. }
  1201. /* Make a nice array of this form:
  1202. Array (
  1203. [0] => "(1.3.6.1.4.1.7165.1.2.2.4 NAME 'gidPool' DESC 'Pool ...)"
  1204. [1] => "(1.3.6.1.4.1.7165.2.2.3 NAME 'sambaAccount' DESC 'Sa ...)"
  1205. etc.) */
  1206. $schema = $schema_entries[0][$schema_to_fetch];
  1207. unset($schema['count']);
  1208. $this->_schema_entries[$schema_to_fetch] = $schema;
  1209. if (DEBUG_ENABLED)
  1210. debug_log('Returning (%s)',25,0,__FILE__,__LINE__,__METHOD__,$schema);
  1211. return $schema;
  1212. }
  1213. /**
  1214. * Gets a single ObjectClass object specified by name.
  1215. *
  1216. * @param string $oclass_name The name of the objectClass to fetch.
  1217. * @param string $dn (optional) It is easier to fetch schema if a DN is provided
  1218. * which defines the subschemaSubEntry attribute (all entries should).
  1219. *
  1220. * @return ObjectClass The specified ObjectClass object or false on error.
  1221. *
  1222. * @see ObjectClass
  1223. * @see SchemaObjectClasses
  1224. */
  1225. public function getSchemaObjectClass($oclass_name,$method=null,$dn='') {
  1226. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1227. debug_log('Entered (%%)',25,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1228. $oclass_name = strtolower($oclass_name);
  1229. $socs = $this->SchemaObjectClasses($method,$dn);
  1230. # Default return value
  1231. $return = false;
  1232. if (isset($socs[$oclass_name]))
  1233. $return = $socs[$oclass_name];
  1234. if (DEBUG_ENABLED)
  1235. debug_log('Returning (%s)',25,0,__FILE__,__LINE__,__METHOD__,$return);
  1236. return $return;
  1237. }
  1238. /**
  1239. * Gets a single AttributeType object specified by name.
  1240. *
  1241. * @param string $oclass_name The name of the AttributeType to fetch.
  1242. * @param string $dn (optional) It is easier to fetch schema if a DN is provided
  1243. * which defines the subschemaSubEntry attribute (all entries should).
  1244. *
  1245. * @return AttributeType The specified AttributeType object or false on error.
  1246. *
  1247. * @see AttributeType
  1248. * @see SchemaAttributes
  1249. */
  1250. public function getSchemaAttribute($attr_name,$method=null,$dn='') {
  1251. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1252. debug_log('Entered (%%)',25,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1253. $attr_name = strtolower($attr_name);
  1254. $sattrs = $this->SchemaAttributes($method,$dn);
  1255. # Default return value
  1256. $return = false;
  1257. if (isset($sattrs[$attr_name]))
  1258. $return = $sattrs[$attr_name];
  1259. if (DEBUG_ENABLED)
  1260. debug_log('Returning (%s)',25,0,__FILE__,__LINE__,__METHOD__,$return);
  1261. return $return;
  1262. }
  1263. /**
  1264. * Gets an associative array of ObjectClass objects for the specified
  1265. * server. Each array entry's key is the name of the objectClass
  1266. * in lower-case and the value is an ObjectClass object.
  1267. *
  1268. * @param string $dn (optional) It is easier to fetch schema if a DN is provided
  1269. * which defines the subschemaSubEntry attribute (all entries should).
  1270. *
  1271. * @return array An array of ObjectClass objects.
  1272. *
  1273. * @see ObjectClass
  1274. * @see getSchemaObjectClass
  1275. */
  1276. public function SchemaObjectClasses($method=null,$dn='') {
  1277. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1278. debug_log('Entered (%%)',25,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1279. # Set default return
  1280. $return = null;
  1281. if ($return = get_cached_item($this->index,'schema','objectclasses')) {
  1282. if (DEBUG_ENABLED)
  1283. debug_log('Returning CACHED [%s] (%s)',25,0,__FILE__,__LINE__,__METHOD__,$this->index,'objectclasses');
  1284. return $return;
  1285. }
  1286. $raw = $this->getRawSchema($method,'objectclasses',$dn);
  1287. if ($raw) {
  1288. # Build the array of objectClasses
  1289. $return = array();
  1290. foreach ($raw as $line) {
  1291. if (is_null($line) || ! strlen($line))
  1292. continue;
  1293. $object_class = new ObjectClass($line,$this);
  1294. $return[$object_class->getName()] = $object_class;
  1295. }
  1296. # Now go through and reference the parent/child relationships
  1297. foreach ($return as $oclass)
  1298. foreach ($oclass->getSupClasses() as $parent_name)
  1299. if (isset($return[strtolower($parent_name)]))
  1300. $return[strtolower($parent_name)]->addChildObjectClass($oclass->getName(false));
  1301. ksort($return);
  1302. # cache the schema to prevent multiple schema fetches from LDAP server
  1303. set_cached_item($this->index,'schema','objectclasses',$return);
  1304. }
  1305. if (DEBUG_ENABLED)
  1306. debug_log('Returning (%s)',25,0,__FILE__,__LINE__,__METHOD__,$return);
  1307. return $return;
  1308. }
  1309. /**
  1310. * Gets an associative array of AttributeType objects for the specified
  1311. * server. Each array entry's key is the name of the attributeType
  1312. * in lower-case and the value is an AttributeType object.
  1313. *
  1314. * @param string $dn (optional) It is easier to fetch schema if a DN is provided
  1315. * which defines the subschemaSubEntry attribute (all entries should).
  1316. *
  1317. * @return array An array of AttributeType objects.
  1318. */
  1319. public function SchemaAttributes($method=null,$dn='') {
  1320. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1321. debug_log('Entered (%%)',25,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1322. # Set default return
  1323. $return = null;
  1324. if ($return = get_cached_item($this->index,'schema','attributes')) {
  1325. if (DEBUG_ENABLED)
  1326. debug_log('(): Returning CACHED [%s] (%s)',25,0,__FILE__,__LINE__,__METHOD__,$this->index,'attributes');
  1327. return $return;
  1328. }
  1329. $raw = $this->getRawSchema($method,'attributeTypes',$dn);
  1330. if ($raw) {
  1331. # build the array of attribueTypes
  1332. $syntaxes = $this->SchemaSyntaxes($method,$dn);
  1333. $attrs = array();
  1334. /**
  1335. * bug 856832: create two arrays - one indexed by name (the standard
  1336. * $attrs array above) and one indexed by oid (the new $attrs_oid array
  1337. * below). This will help for directory servers, like IBM's, that use OIDs
  1338. * in their attribute definitions of SUP, etc
  1339. */
  1340. $attrs_oid = array();
  1341. foreach ($raw as $line) {
  1342. if (is_null($line) || ! strlen($line))
  1343. continue;
  1344. $attr = new AttributeType($line);
  1345. if (isset($syntaxes[$attr->getSyntaxOID()])) {
  1346. $syntax = $syntaxes[$attr->getSyntaxOID()];
  1347. $attr->setType($syntax->getDescription());
  1348. }
  1349. $attrs[$attr->getName()] = $attr;
  1350. /**
  1351. * bug 856832: create an entry in the $attrs_oid array too. This
  1352. * will be a ref to the $attrs entry for maintenance and performance
  1353. * reasons
  1354. */
  1355. $attrs_oid[$attr->getOID()] = &$attrs[$attr->getName()];
  1356. }
  1357. # go back and add data from aliased attributeTypes
  1358. foreach ($attrs as $name => $attr) {
  1359. $aliases = $attr->getAliases();
  1360. if (is_array($aliases) && count($aliases) > 0) {
  1361. /* foreach of the attribute's aliases, create a new entry in the attrs array
  1362. * with its name set to the alias name, and all other data copied.*/
  1363. foreach ($aliases as $alias_attr_name) {
  1364. $new_attr = clone $attr;
  1365. $new_attr->setName($alias_attr_name);
  1366. $new_attr->addAlias($attr->getName(false));
  1367. $new_attr->removeAlias($alias_attr_name);
  1368. $new_attr_key = strtolower($alias_attr_name);
  1369. $attrs[$new_attr_key] = $new_attr;
  1370. }
  1371. }
  1372. }
  1373. # go back and add any inherited descriptions from parent attributes (ie, cn inherits name)
  1374. foreach ($attrs as $key => $attr) {
  1375. $sup_attr_name = $attr->getSupAttribute();
  1376. $sup_attr = null;
  1377. if (trim($sup_attr_name)) {
  1378. /* This loop really should traverse infinite levels of inheritance (SUP) for attributeTypes,
  1379. * but just in case we get carried away, stop at 100. This shouldn't happen, but for
  1380. * some weird reason, we have had someone report that it has happened. Oh well.*/
  1381. $i = 0;
  1382. while ($i++<100 /** 100 == INFINITY ;) */) {
  1383. if (isset($attrs_oid[$sup_attr_name])) {
  1384. $attr->setSupAttribute($attrs_oid[$sup_attr_name]->getName());
  1385. $sup_attr_name = $attr->getSupAttribute();
  1386. }
  1387. if (! isset($attrs[strtolower($sup_attr_name)])){
  1388. error(sprintf('Schema error: attributeType "%s" inherits from "%s", but attributeType "%s" does not exist.',
  1389. $attr->getName(),$sup_attr_name,$sup_attr_name),'error','index.php');
  1390. return;
  1391. }
  1392. $sup_attr = $attrs[strtolower($sup_attr_name)];
  1393. $sup_attr_name = $sup_attr->getSupAttribute();
  1394. # Does this superior attributeType not have a superior attributeType?
  1395. if (is_null($sup_attr_name) || strlen(trim($sup_attr_name)) == 0) {
  1396. /* Since this attribute's superior attribute does not have another superior
  1397. * attribute, clone its properties for this attribute. Then, replace
  1398. * those cloned values with those that can be explicitly set by the child
  1399. * attribute attr). Save those few properties which the child can set here:*/
  1400. $tmp_name = $attr->getName(false);
  1401. $tmp_oid = $attr->getOID();
  1402. $tmp_sup = $attr->getSupAttribute();
  1403. $tmp_aliases = $attr->getAliases();
  1404. $tmp_single_val = $attr->getIsSingleValue();
  1405. $tmp_desc = $attr->getDescription();
  1406. /* clone the SUP attributeType and populate those values
  1407. * that were set by the child attributeType */
  1408. $attr = clone $sup_attr;
  1409. $attr->setOID($tmp_oid);
  1410. $attr->setName($tmp_name);
  1411. $attr->setSupAttribute($tmp_sup);
  1412. $attr->setAliases($tmp_aliases);
  1413. $attr->setDescription($tmp_desc);
  1414. /* only overwrite the SINGLE-VALUE property if the child explicitly sets it
  1415. * (note: All LDAP attributes default to multi-value if not explicitly set SINGLE-VALUE) */
  1416. if ($tmp_single_val)
  1417. $attr->setIsSingleValue(true);
  1418. /* replace this attribute in the attrs array now that we have populated
  1419. new values therein */
  1420. $attrs[$key] = $attr;
  1421. # very important: break out after we are done with this attribute
  1422. $sup_attr_name = null;
  1423. $sup_attr = null;
  1424. break;
  1425. }
  1426. }
  1427. }
  1428. }
  1429. ksort($attrs);
  1430. # Add the used in and required_by values.
  1431. $socs = $this->SchemaObjectClasses($method);
  1432. if (! is_array($socs))
  1433. return array();
  1434. foreach ($socs as $object_class) {
  1435. $must_attrs = $object_class->getMustAttrNames();
  1436. $may_attrs = $object_class->getMayAttrNames();
  1437. $oclass_attrs = array_unique(array_merge($must_attrs,$may_attrs));
  1438. # Add Used In.
  1439. foreach ($oclass_attrs as $attr_name)
  1440. if (isset($attrs[strtolower($attr_name)]))
  1441. $attrs[strtolower($attr_name)]->addUsedInObjectClass($object_class->getName(false));
  1442. # Add Required By.
  1443. foreach ($must_attrs as $attr_name)
  1444. if (isset($attrs[strtolower($attr_name)]))
  1445. $attrs[strtolower($attr_name)]->addRequiredByObjectClass($object_class->getName(false));
  1446. # Force May
  1447. foreach ($object_class->getForceMayAttrs() as $attr_name)
  1448. if (isset($attrs[strtolower($attr_name->name)]))
  1449. $attrs[strtolower($attr_name->name)]->setForceMay();
  1450. }
  1451. $return = $attrs;
  1452. # cache the schema to prevent multiple schema fetches from LDAP server
  1453. set_cached_item($this->index,'schema','attributes',$return);
  1454. }
  1455. if (DEBUG_ENABLED)
  1456. debug_log('Returning (%s)',25,0,__FILE__,__LINE__,__METHOD__,$return);
  1457. return $return;
  1458. }
  1459. /**
  1460. * Returns an array of MatchingRule objects for the specified server.
  1461. * The key of each entry is the OID of the matching rule.
  1462. */
  1463. public function MatchingRules($method=null,$dn='') {
  1464. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1465. debug_log('Entered (%%)',25,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1466. # Set default return
  1467. $return = null;
  1468. if ($return = get_cached_item($this->index,'schema','matchingrules')) {
  1469. if (DEBUG_ENABLED)
  1470. debug_log('Returning CACHED [%s] (%s).',25,0,__FILE__,__LINE__,__METHOD__,$this->index,'matchingrules');
  1471. return $return;
  1472. }
  1473. # build the array of MatchingRule objects
  1474. $raw = $this->getRawSchema($method,'matchingRules',$dn);
  1475. if ($raw) {
  1476. $rules = array();
  1477. foreach ($raw as $line) {
  1478. if (is_null($line) || ! strlen($line))
  1479. continue;
  1480. $rule = new MatchingRule($line);
  1481. $key = $rule->getName();
  1482. $rules[$key] = $rule;
  1483. }
  1484. ksort($rules);
  1485. /* For each MatchingRuleUse entry, add the attributes who use it to the
  1486. * MatchingRule in the $rules array.*/
  1487. $raw = $this->getRawSchema($method,'matchingRuleUse');
  1488. if ($raw != false) {
  1489. foreach ($raw as $line) {
  1490. if (is_null($line) || ! strlen($line))
  1491. continue;
  1492. $rule_use = new MatchingRuleUse($line);
  1493. $key = $rule_use->getName();
  1494. if (isset($rules[$key]))
  1495. $rules[$key]->setUsedByAttrs($rule_use->getUsedByAttrs());
  1496. }
  1497. } else {
  1498. /* No MatchingRuleUse entry in the subschema, so brute-forcing
  1499. * the reverse-map for the "$rule->getUsedByAttrs()" data.*/
  1500. $sattrs = $this->SchemaAttributes($method,$dn);
  1501. if (is_array($sattrs))
  1502. foreach ($sattrs as $attr) {
  1503. $rule_key = strtolower($attr->getEquality());
  1504. if (isset($rules[$rule_key]))
  1505. $rules[$rule_key]->addUsedByAttr($attr->getName(false));
  1506. }
  1507. }
  1508. $return = $rules;
  1509. # cache the schema to prevent multiple schema fetches from LDAP server
  1510. set_cached_item($this->index,'schema','matchingrules',$return);
  1511. }
  1512. if (DEBUG_ENABLED)
  1513. debug_log('Returning (%s)',25,0,__FILE__,__LINE__,__METHOD__,$return);
  1514. return $return;
  1515. }
  1516. /**
  1517. * Returns an array of Syntax objects that this LDAP server uses mapped to
  1518. * their descriptions. The key of each entry is the OID of the Syntax.
  1519. */
  1520. public function SchemaSyntaxes($method=null,$dn='') {
  1521. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1522. debug_log('Entered (%%)',25,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1523. # Set default return
  1524. $return = null;
  1525. if ($return = get_cached_item($this->index,'schema','syntaxes')) {
  1526. if (DEBUG_ENABLED)
  1527. debug_log('Returning CACHED [%s] (%s).',25,0,__FILE__,__LINE__,__METHOD__,$this->index,'syntaxes');
  1528. return $return;
  1529. }
  1530. $raw = $this->getRawSchema($method,'ldapSyntaxes',$dn);
  1531. if ($raw) {
  1532. # build the array of attributes
  1533. $return = array();
  1534. foreach ($raw as $line) {
  1535. if (is_null($line) || ! strlen($line))
  1536. continue;
  1537. $syntax = new Syntax($line);
  1538. $key = strtolower(trim($syntax->getOID()));
  1539. if (! $key)
  1540. continue;
  1541. $return[$key] = $syntax;
  1542. }
  1543. ksort($return);
  1544. # cache the schema to prevent multiple schema fetches from LDAP server
  1545. set_cached_item($this->index,'schema','syntaxes',$return);
  1546. }
  1547. if (DEBUG_ENABLED)
  1548. debug_log('Returning (%s)',25,0,__FILE__,__LINE__,__METHOD__,$return);
  1549. return $return;
  1550. }
  1551. /**
  1552. * This function determines if the specified attribute is contained in the force_may list
  1553. * as configured in config.php.
  1554. *
  1555. * @return boolean True if the specified attribute is configured to be force as a may attribute
  1556. */
  1557. function isForceMay($attr_name) {
  1558. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1559. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1560. return in_array($attr_name,unserialize(strtolower(serialize($this->getValue('server','force_may')))));
  1561. }
  1562. /**
  1563. * Much like getDNAttrValues(), but only returns the values for
  1564. * one attribute of an object. Example calls:
  1565. *
  1566. * <code>
  1567. * print_r(getDNAttrValue('cn=Bob,ou=people,dc=example,dc=com','sn'));
  1568. * Array (
  1569. * [0] => Smith
  1570. * )
  1571. *
  1572. * print_r(getDNAttrValue('cn=Bob,ou=people,dc=example,dc=com','objectClass'));
  1573. * Array (
  1574. * [0] => top
  1575. * [1] => person
  1576. * )
  1577. * </code>
  1578. *
  1579. * @param string The distinguished name (DN) of the entry whose attributes/values to fetch.
  1580. * @param string The attribute whose value(s) to return (ie, "objectClass", "cn", "userPassword")
  1581. * @param string Which connection method resource to use
  1582. * @param constant For aliases and referrals, this parameter specifies whether to
  1583. * follow references to the referenced DN or to fetch the attributes for
  1584. * the referencing DN. See http://php.net/ldap_search for the 4 valid
  1585. * options.
  1586. * @return array
  1587. * @see getDNAttrValues
  1588. * @todo Caching these values may be problematic with multiple calls and different deref values.
  1589. */
  1590. public function getDNAttrValue($dn,$attr,$method=null,$deref=LDAP_DEREF_NEVER) {
  1591. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1592. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1593. # Ensure our attr is in lowercase
  1594. $attr = strtolower($attr);
  1595. $values = $this->getDNAttrValues($dn,$method,$deref);
  1596. if (isset($values[$attr]))
  1597. return $values[$attr];
  1598. else
  1599. return array();
  1600. }
  1601. /**
  1602. * Gets the attributes/values of an entry. Returns an associative array whose
  1603. * keys are attribute value names and whose values are arrays of values for
  1604. * said attribute.
  1605. *
  1606. * Optionally, callers may specify true for the parameter
  1607. * $lower_case_attr_names to force all keys in the associate array (attribute
  1608. * names) to be lower case.
  1609. *
  1610. * Example of its usage:
  1611. * <code>
  1612. * print_r(getDNAttrValues('cn=Bob,ou=pepole,dc=example,dc=com')
  1613. * Array (
  1614. * [objectClass] => Array (
  1615. * [0] => person
  1616. * [1] => top
  1617. * )
  1618. * [cn] => Array (
  1619. * [0] => Bob
  1620. * )
  1621. * [sn] => Array (
  1622. * [0] => Jones
  1623. * )
  1624. * [dn] => Array (
  1625. * [0] => cn=Bob,ou=pepole,dc=example,dc=com
  1626. * )
  1627. * )
  1628. * </code>
  1629. *
  1630. * @param string The distinguished name (DN) of the entry whose attributes/values to fetch.
  1631. * @param string Which connection method resource to use
  1632. * @param constant For aliases and referrals, this parameter specifies whether to
  1633. * follow references to the referenced DN or to fetch the attributes for
  1634. * the referencing DN. See http://php.net/ldap_search for the 4 valid
  1635. * options.
  1636. * @return array
  1637. * @see getDNSysAttrs
  1638. * @see getDNAttrValue
  1639. */
  1640. public function getDNAttrValues($dn,$method=null,$deref=LDAP_DEREF_NEVER,$attrs=array('*','+'),$nocache=false) {
  1641. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1642. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1643. static $CACHE;
  1644. $cacheindex = null;
  1645. $method = $this->getMethod($method);
  1646. if (in_array('*',$attrs) && in_array('+',$attrs))
  1647. $cacheindex = '&';
  1648. elseif (in_array('+',$attrs))
  1649. $cacheindex = '+';
  1650. elseif (in_array('*',$attrs))
  1651. $cacheindex = '*';
  1652. if (! $nocache && ! is_null($cacheindex) && isset($CACHE[$this->index][$method][$dn][$cacheindex])) {
  1653. $results = $CACHE[$this->index][$method][$dn][$cacheindex];
  1654. if (DEBUG_ENABLED)
  1655. debug_log('Returning (%s)',17,0,__FILE__,__LINE__,__METHOD__,$results);
  1656. } else {
  1657. $query = array();
  1658. $query['base'] = $this->escapeDN($dn);
  1659. $query['scope'] = 'base';
  1660. $query['deref'] = $deref;
  1661. $query['attrs'] = $attrs;
  1662. $query['baseok'] = true;
  1663. $results = $this->query($query,$method);
  1664. if (count($results))
  1665. $results = array_pop($results);
  1666. $results = array_change_key_case($results);
  1667. # Covert all our result key values to an array
  1668. foreach ($results as $key => $values)
  1669. if (! is_array($results[$key]))
  1670. $results[$key] = array($results[$key]);
  1671. # Finally sort the results
  1672. ksort($results);
  1673. if (! is_null($cacheindex) && count($results))
  1674. $CACHE[$this->index][$method][$dn][$cacheindex] = $results;
  1675. }
  1676. return $results;
  1677. }
  1678. /**
  1679. * Returns true if the attribute specified is required to take as input a DN.
  1680. * Some examples include 'distinguishedName', 'member' and 'uniqueMember'.
  1681. *
  1682. * @param string $attr_name The name of the attribute of interest (case insensitive)
  1683. * @return boolean
  1684. */
  1685. function isDNAttr($attr_name,$method=null) {
  1686. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1687. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1688. # Simple test first
  1689. $dn_attrs = array('aliasedObjectName');
  1690. foreach ($dn_attrs as $dn_attr)
  1691. if (strcasecmp($attr_name,$dn_attr) == 0)
  1692. return true;
  1693. # Now look at the schema OID
  1694. $sattr = $this->getSchemaAttribute($attr_name);
  1695. if (! $sattr)
  1696. return false;
  1697. $syntax_oid = $sattr->getSyntaxOID();
  1698. if ('1.3.6.1.4.1.1466.115.121.1.12' == $syntax_oid)
  1699. return true;
  1700. if ('1.3.6.1.4.1.1466.115.121.1.34' == $syntax_oid)
  1701. return true;
  1702. $syntaxes = $this->SchemaSyntaxes($method);
  1703. if (! isset($syntaxes[$syntax_oid]))
  1704. return false;
  1705. $syntax_desc = $syntaxes[ $syntax_oid ]->getDescription();
  1706. if (strpos(strtolower($syntax_desc),'distinguished name'))
  1707. return true;
  1708. return false;
  1709. }
  1710. /**
  1711. * Used to determine if the specified attribute is indeed a jpegPhoto. If the
  1712. * specified attribute is one that houses jpeg data, true is returned. Otherwise
  1713. * this function returns false.
  1714. *
  1715. * @param string $attr_name The name of the attribute to test.
  1716. * @return boolean
  1717. * @see draw_jpeg_photo
  1718. */
  1719. function isJpegPhoto($attr_name) {
  1720. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1721. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1722. # easy quick check
  1723. if (! strcasecmp($attr_name,'jpegPhoto') || ! strcasecmp($attr_name,'photo'))
  1724. return true;
  1725. # go to the schema and get the Syntax OID
  1726. $sattr = $this->getSchemaAttribute($attr_name);
  1727. if (! $sattr)
  1728. return false;
  1729. $oid = $sattr->getSyntaxOID();
  1730. $type = $sattr->getType();
  1731. if (! strcasecmp($type,'JPEG') || ($oid == '1.3.6.1.4.1.1466.115.121.1.28'))
  1732. return true;
  1733. return false;
  1734. }
  1735. /**
  1736. * Given an attribute name and server ID number, this function returns
  1737. * whether the attrbiute contains boolean data. This is useful for
  1738. * developers who wish to display the contents of a boolean attribute
  1739. * with a drop-down.
  1740. *
  1741. * @param string $attr_name The name of the attribute to test.
  1742. * @return boolean
  1743. */
  1744. function isAttrBoolean($attr_name) {
  1745. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1746. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1747. $type = ($sattr = $this->getSchemaAttribute($attr_name)) ? $sattr->getType() : null;
  1748. if (! strcasecmp('boolean',$type) ||
  1749. ! strcasecmp('isCriticalSystemObject',$attr_name) ||
  1750. ! strcasecmp('showInAdvancedViewOnly',$attr_name))
  1751. return true;
  1752. else
  1753. return false;
  1754. }
  1755. /**
  1756. * Given an attribute name and server ID number, this function returns
  1757. * whether the attribute may contain binary data. This is useful for
  1758. * developers who wish to display the contents of an arbitrary attribute
  1759. * but don't want to dump binary data on the page.
  1760. *
  1761. * @param string $attr_name The name of the attribute to test.
  1762. * @return boolean
  1763. *
  1764. * @see isJpegPhoto
  1765. */
  1766. function isAttrBinary($attr_name) {
  1767. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1768. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1769. /**
  1770. * Determining if an attribute is binary can be an expensive operation.
  1771. * We cache the results for each attr name on each server in the $attr_cache
  1772. * to speed up subsequent calls. The $attr_cache looks like this:
  1773. *
  1774. * Array
  1775. * 0 => Array
  1776. * 'objectclass' => false
  1777. * 'cn' => false
  1778. * 'usercertificate' => true
  1779. * 1 => Array
  1780. * 'jpegphoto' => true
  1781. * 'cn' => false
  1782. */
  1783. static $attr_cache;
  1784. $attr_name = strtolower($attr_name);
  1785. if (isset($attr_cache[$this->index][$attr_name]))
  1786. return $attr_cache[$this->index][$attr_name];
  1787. if ($attr_name == 'userpassword') {
  1788. $attr_cache[$this->index][$attr_name] = false;
  1789. return false;
  1790. }
  1791. # Quick check: If the attr name ends in ";binary", then it's binary.
  1792. if (strcasecmp(substr($attr_name,strlen($attr_name) - 7),';binary') == 0) {
  1793. $attr_cache[$this->index][$attr_name] = true;
  1794. return true;
  1795. }
  1796. # See what the server schema says about this attribute
  1797. $sattr = $this->getSchemaAttribute($attr_name);
  1798. if (! is_object($sattr)) {
  1799. /* Strangely, some attributeTypes may not show up in the server
  1800. * schema. This behavior has been observed in MS Active Directory.*/
  1801. $type = null;
  1802. $syntax = null;
  1803. } else {
  1804. $type = $sattr->getType();
  1805. $syntax = $sattr->getSyntaxOID();
  1806. }
  1807. if (strcasecmp($type,'Certificate') == 0 ||
  1808. strcasecmp($type,'Binary') == 0 ||
  1809. strcasecmp($attr_name,'usercertificate') == 0 ||
  1810. strcasecmp($attr_name,'usersmimecertificate') == 0 ||
  1811. strcasecmp($attr_name,'networkaddress') == 0 ||
  1812. strcasecmp($attr_name,'objectGUID') == 0 ||
  1813. strcasecmp($attr_name,'objectSID') == 0 ||
  1814. strcasecmp($attr_name,'auditingPolicy') == 0 ||
  1815. strcasecmp($attr_name,'jpegPhoto') == 0 ||
  1816. $syntax == '1.3.6.1.4.1.1466.115.121.1.10' ||
  1817. $syntax == '1.3.6.1.4.1.1466.115.121.1.28' ||
  1818. $syntax == '1.3.6.1.4.1.1466.115.121.1.5' ||
  1819. $syntax == '1.3.6.1.4.1.1466.115.121.1.8' ||
  1820. $syntax == '1.3.6.1.4.1.1466.115.121.1.9'
  1821. ) {
  1822. $attr_cache[$this->index][$attr_name] = true;
  1823. return true;
  1824. } else {
  1825. $attr_cache[$this->index][$attr_name] = false;
  1826. return false;
  1827. }
  1828. }
  1829. /**
  1830. * This function will test if a user is a member of a group.
  1831. *
  1832. * Inputs:
  1833. * @param string $user membership value that is being checked
  1834. * @param dn $group DN to see if user is a member
  1835. * @return bool true|false
  1836. */
  1837. function userIsMember($user,$group) {
  1838. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1839. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1840. $user = strtolower($user);
  1841. $group = $this->getDNAttrValues($group);
  1842. # If you are using groupOfNames objectClass
  1843. if (array_key_exists('member',$group) && ! is_array($group['member']))
  1844. $group['member'] = array($group['member']);
  1845. if (array_key_exists('member',$group) &&
  1846. in_array($user,arrayLower($group['member'])))
  1847. return true;
  1848. # If you are using groupOfUniqueNames objectClass
  1849. if (array_key_exists('uniquemember',$group) && ! is_array($group['uniquemember']))
  1850. $group['uniquemember'] = array($group['uniquemember']);
  1851. if (array_key_exists('uniquemember',$group) &&
  1852. in_array($user,arrayLower($group['uniquemember'])))
  1853. return true;
  1854. return false;
  1855. }
  1856. /**
  1857. * This function will determine if the user is allowed to login based on a filter
  1858. */
  1859. protected function userIsAllowedLogin($dn) {
  1860. if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  1861. debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  1862. $dn = trim(strtolower($dn));
  1863. if (! $this->getValue('login','allowed_dns'))
  1864. return true;
  1865. foreach ($this->getValue('login','allowed_dns') as $login_allowed_dn) {
  1866. if (DEBUG_ENABLED)
  1867. debug_log('Working through (%s)',80,0,__FILE__,__LINE__,__METHOD__,$login_allowed_dn);
  1868. /* Check if $login_allowed_dn is an ldap search filter
  1869. * Is first occurence of 'filter=' (case ensitive) at position 0 ? */
  1870. if (preg_match('/^\([&|]\(/',$login_allowed_dn)) {
  1871. $query = array();
  1872. $query['filter'] = $login_allowed_dn;
  1873. $query['attrs'] = array('dn');
  1874. foreach($this->getBaseDN() as $base_dn) {
  1875. $query['base'] = $base_dn;
  1876. $results = $this->query($query,null);
  1877. if (DEBUG_ENABLED)
  1878. debug_log('Search, Filter [%s], BaseDN [%s] Results [%s]',16,0,__FILE__,__LINE__,__METHOD__,
  1879. $query['filter'],$query['base'],$results);
  1880. if ($results) {
  1881. $dn_array = array();
  1882. foreach ($results as $result)
  1883. array_push($dn_array,$result['dn']);
  1884. $dn_array = array_unique($dn_array);
  1885. if (count($dn_array))
  1886. foreach ($dn_array as $result_dn) {
  1887. if (DEBUG_ENABLED)
  1888. debug_log('Comparing with [%s]',80,0,__FILE__,__LINE__,__METHOD__,$result_dn);
  1889. # Check if $result_dn is a user DN
  1890. if (strcasecmp($dn,trim(strtolower($result_dn))) == 0)
  1891. return true;
  1892. # Check if $result_dn is a group DN
  1893. if ($this->userIsMember($dn,$result_dn))
  1894. return true;
  1895. }
  1896. }
  1897. }
  1898. }
  1899. # Check if $login_allowed_dn is a user DN
  1900. if (strcasecmp($dn,trim(strtolower($login_allowed_dn))) == 0)
  1901. return true;
  1902. # Check if $login_allowed_dn is a group DN
  1903. if ($this->userIsMember($dn,$login_allowed_dn))
  1904. return true;
  1905. }
  1906. return false;
  1907. }
  1908. }
  1909. ?>