PageRenderTime 71ms CodeModel.GetById 15ms app.highlight 42ms RepoModel.GetById 1ms app.codeStats 1ms

/phpldapadmin-1.2.2/lib/ds_ldap.php

#
PHP | 2401 lines | 1288 code | 488 blank | 625 comment | 432 complexity | 7c4cddc58c3ee37000b35bb156cf6b7f MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   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/**
  10 * This abstract class provides the basic variables and methods for LDAP datastores
  11 *
  12 * @package phpLDAPadmin
  13 * @subpackage DataStore
  14 */
  15class ldap extends DS {
  16	# If we fail to connect, set this to true
  17	private $noconnect = false;
  18	# Raw Schema entries
  19	private $_schema_entries = null;
  20	# Schema DN
  21	private $_schemaDN = null;
  22
  23	public function __construct($index) {
  24		if (defined('DEBUG_ENABLED') && DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
  25			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
  26
  27		$this->index = $index;
  28		$this->type = 'ldap';
  29
  30		# Additional values that can go in our config.php
  31		$this->custom = new StdClass;
  32		$this->default = new StdClass;
  33
  34/*
  35 * Not used by PLA
  36		# Database Server Variables
  37		$this->default->server['db'] = array(
  38			'desc'=>'Database Name',
  39			'untested'=>true,
  40			'default'=>null);
  41*/
  42
  43		/* This was created for IDS - since it doesnt present STRUCTURAL against objectClasses
  44		 * definitions when reading the schema.*/
  45		$this->default->server['schema_oclass_default'] = array(
  46			'desc'=>'When reading the schema, and it doesnt specify objectClass type, default it to this',
  47			'default'=>null);
  48
  49		$this->default->server['base'] = array(
  50			'desc'=>'LDAP Base DNs',
  51			'default'=>array());
  52
  53		$this->default->server['tls'] = array(
  54			'desc'=>'Connect using TLS',
  55			'default'=>false);
  56
  57		# Login Details
  58		$this->default->login['attr'] = array(
  59			'desc'=>'Attribute to use to find the users DN',
  60			'default'=>'dn');
  61
  62		$this->default->login['anon_bind'] = array(
  63			'desc'=>'Enable anonymous bind logins',
  64			'default'=>true);
  65
  66		$this->default->login['allowed_dns'] = array(
  67			'desc'=>'Limit logins to users who match any of the following LDAP filters',
  68			'default'=>array());
  69
  70		$this->default->login['base'] = array(
  71			'desc'=>'Limit logins to users who are in these base DNs',
  72			'default'=>array());
  73
  74		$this->default->login['class'] = array(
  75			'desc'=>'Strict login to users containing a specific objectClasses',
  76			'default'=>array());
  77
  78		$this->default->proxy['attr'] = array(
  79			'desc'=>'Attribute to use to find the users DN for proxy based authentication',
  80			'default'=>array());
  81
  82		# SASL configuration
  83		$this->default->sasl['mech'] = array(
  84			'desc'=>'SASL mechanism used while binding LDAP server',
  85			'default'=>'GSSAPI');
  86
  87		$this->default->sasl['realm'] = array(
  88			'desc'=>'SASL realm name',
  89			'untested'=>true,
  90			'default'=>null);
  91
  92		$this->default->sasl['authz_id'] = array(
  93			'desc'=>'SASL authorization id',
  94			'untested'=>true,
  95			'default'=>null);
  96
  97		$this->default->sasl['authz_id_regex'] = array(
  98			'desc'=>'SASL authorization id PCRE regular expression',
  99			'untested'=>true,
 100			'default'=>null);
 101
 102		$this->default->sasl['authz_id_replacement'] = array(
 103			'desc'=>'SASL authorization id PCRE regular expression replacement string',
 104			'untested'=>true,
 105			'default'=>null);
 106
 107		$this->default->sasl['props'] = array(
 108			'desc'=>'SASL properties',
 109			'untested'=>true,
 110			'default'=>null);
 111	}
 112
 113	/**
 114	 * Required ABSTRACT functions
 115	 */
 116	/**
 117	 * Connect and Bind to the Database
 118	 *
 119	 * @param string Which connection method resource to use
 120	 * @return resource|null Connection resource if successful, null if not.
 121	 */
 122	protected function connect($method,$debug=false,$new=false) {
 123		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 124			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 125
 126		static $CACHE = array();
 127
 128		$method = $this->getMethod($method);
 129		$bind = array();
 130
 131		if (isset($CACHE[$this->index][$method]) && $CACHE[$this->index][$method])
 132			return $CACHE[$this->index][$method];
 133
 134		# Check if we have logged in and therefore need to use those details as our bind.
 135		$bind['id'] = is_null($this->getLogin($method)) && $method != 'anon' ? $this->getLogin('user') : $this->getLogin($method);
 136		$bind['pass'] = is_null($this->getPassword($method)) && $method != 'anon' ? $this->getPassword('user') : $this->getPassword($method);
 137
 138		# If our bind id is still null, we are not logged in.
 139		if (is_null($bind['id']) && ! in_array($method,array('anon','login')))
 140			return null;
 141
 142		# If we bound to the LDAP server with these details for a different connection, return that resource
 143		if (isset($CACHE[$this->index]) && ! $new)
 144			foreach ($CACHE[$this->index] as $cachedmethod => $resource) {
 145				if (($this->getLogin($cachedmethod) == $bind['id']) && ($this->getPassword($cachedmethod) == $bind['pass'])) {
 146					$CACHE[$this->index][$method] = $resource;
 147
 148					return $CACHE[$this->index][$method];
 149				}
 150			}
 151
 152		$CACHE[$this->index][$method] = null;
 153
 154		# No identifiable connection exists, lets create a new one.
 155		if (DEBUG_ENABLED)
 156			debug_log('Creating NEW connection [%s] for index [%s]',16,0,__FILE__,__LINE__,__METHOD__,
 157				$method,$this->index);
 158
 159		if (function_exists('run_hook'))
 160			run_hook('pre_connect',array('server_id'=>$this->index,'method'=>$method));
 161
 162		if ($this->getValue('server','port'))
 163			$resource = ldap_connect($this->getValue('server','host'),$this->getValue('server','port'));
 164		else
 165			$resource = ldap_connect($this->getValue('server','host'));
 166
 167		$CACHE[$this->index][$method] = $resource;
 168
 169		if (DEBUG_ENABLED)
 170			debug_log('LDAP Resource [%s], Host [%s], Port [%s]',16,0,__FILE__,__LINE__,__METHOD__,
 171				$resource,$this->getValue('server','host'),$this->getValue('server','port'));
 172
 173		if (! is_resource($resource))
 174			debug_dump_backtrace('UNHANDLED, $resource is not a resource',1);
 175
 176		# Go with LDAP version 3 if possible (needed for renaming and Novell schema fetching)
 177		ldap_set_option($resource,LDAP_OPT_PROTOCOL_VERSION,3);
 178
 179		/* Disabling this makes it possible to browse the tree for Active Directory, and seems
 180		 * to not affect other LDAP servers (tested with OpenLDAP) as phpLDAPadmin explicitly
 181		 * specifies deref behavior for each ldap_search operation. */
 182		ldap_set_option($resource,LDAP_OPT_REFERRALS,0);
 183
 184		# Try to fire up TLS is specified in the config
 185		if ($this->isTLSEnabled())
 186			$this->startTLS($resource);
 187
 188		# If SASL has been configured for binding, then start it now.
 189		if ($this->isSASLEnabled())
 190			$bind['result'] = $this->startSASL($resource,$method);
 191
 192		# Normal bind...
 193		else
 194			$bind['result'] = @ldap_bind($resource,$bind['id'],$bind['pass']);
 195
 196		if ($debug)
 197			debug_dump(array('method'=>$method,'bind'=>$bind,'USER'=>$_SESSION['USER']));
 198
 199		if (DEBUG_ENABLED)
 200			debug_log('Resource [%s], Bind Result [%s]',16,0,__FILE__,__LINE__,__METHOD__,$resource,$bind);
 201
 202		if (! $bind['result']) {
 203			if (DEBUG_ENABLED)
 204				debug_log('Leaving with FALSE, bind FAILed',16,0,__FILE__,__LINE__,__METHOD__);
 205
 206			$this->noconnect = true;
 207
 208			system_message(array(
 209				'title'=>sprintf('%s %s',_('Unable to connect to LDAP server'),$this->getName()),
 210				'body'=>sprintf('<b>%s</b>: %s (%s) for <b>%s</b>',_('Error'),$this->getErrorMessage($method),$this->getErrorNum($method),$method),
 211				'type'=>'error'));
 212
 213			$CACHE[$this->index][$method] = null;
 214
 215		} else {
 216			$this->noconnect = false;
 217
 218			# If this is a proxy session, we need to switch to the proxy user
 219			if ($this->isProxyEnabled() && $bind['id'] && $method != 'anon')
 220				if (! $this->startProxy($resource,$method)) {
 221					$this->noconnect = true;
 222					$CACHE[$this->index][$method] = null;
 223				}
 224		}
 225
 226		if (function_exists('run_hook'))
 227			run_hook('post_connect',array('server_id'=>$this->index,'method'=>$method,'id'=>$bind['id']));
 228
 229		if ($debug)
 230			debug_dump(array($method=>$CACHE[$this->index][$method]));
 231
 232		return $CACHE[$this->index][$method];
 233	}
 234
 235	/**
 236	 * Login to the database with the application user/password
 237	 *
 238	 * @return boolean true|false for successful login.
 239	 */
 240	public function login($user=null,$pass=null,$method=null,$new=false) {
 241		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 242			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 243
 244		$userDN = null;
 245
 246		# Get the userDN from the username.
 247		if (! is_null($user)) {
 248			# If login,attr is set to DN, then user should be a DN
 249			if (($this->getValue('login','attr') == 'dn') || $method != 'user')
 250				$userDN = $user;
 251			else
 252				$userDN = $this->getLoginID($user,'login');
 253
 254			if (! $userDN && $this->getValue('login','fallback_dn'))
 255				$userDN = $user;
 256
 257			if (! $userDN)
 258				return false;
 259
 260		} else {
 261			if (in_array($method,array('user','anon'))) {
 262				$method = 'anon';
 263				$userDN = '';
 264				$pass = '';
 265
 266			} else {
 267				$userDN = $this->getLogin('user');
 268				$pass = $this->getPassword('user');
 269			}
 270		}
 271
 272		if (! $this->isAnonBindAllowed() && ! trim($userDN))
 273			return false;
 274
 275		# Temporarily set our user details
 276		$this->setLogin($userDN,$pass,$method);
 277
 278		$connect = $this->connect($method,false,$new);
 279
 280		# If we didnt log in...
 281		if (! is_resource($connect) || $this->noconnect || ! $this->userIsAllowedLogin($userDN)) {
 282			$this->logout($method);
 283
 284			return false;
 285
 286		} else
 287			return true;
 288	}
 289
 290	/**
 291	 * Perform a query to the Database
 292	 *
 293	 * @param string query to perform
 294	 *	$query['base']
 295	 *	$query['filter']
 296	 *	$query['scope']
 297	 *	$query['attrs'] = array();
 298	 *	$query['deref']
 299	 * @param string Which connection method resource to use
 300	 * @param string Index items according to this key
 301	 * @param boolean Enable debugging output
 302	 * @return array|null Results of query.
 303	 */
 304	public function query($query,$method,$index=null,$debug=false) {
 305		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 306			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 307
 308		$attrs_only = 0;
 309
 310		# Defaults
 311		if (! isset($query['attrs']))
 312			$query['attrs'] = array();
 313		else
 314			# Re-index the attrs, PHP throws an error if the keys are not sequential from 0.
 315			$query['attrs'] = array_values($query['attrs']);
 316
 317		if (! isset($query['base'])) {
 318			$bases = $this->getBaseDN();
 319			$query['base'] = array_shift($bases);
 320		}
 321
 322		if (! isset($query['deref']))
 323			$query['deref'] = $_SESSION[APPCONFIG]->getValue('deref','search');
 324		if (! isset($query['filter']))
 325			$query['filter'] = '(&(objectClass=*))';
 326		if (! isset($query['scope']))
 327			$query['scope'] = 'sub';
 328		if (! isset($query['size_limit']))
 329			$query['size_limit'] = 0;
 330		if (! isset($query['time_limit']))
 331			$query['time_limit'] = 0;
 332
 333		if ($query['scope'] == 'base' && ! isset($query['baseok']))
 334			system_message(array(
 335				'title'=>sprintf('Dont call %s',__METHOD__),
 336				'body'=>sprintf('Use getDNAttrValues for base queries [%s]',$query['base']),
 337				'type'=>'info'));
 338
 339		if (is_array($query['base'])) {
 340			system_message(array(
 341				'title'=>_('Invalid BASE for query'),
 342				'body'=>_('The query was cancelled because of an invalid base.'),
 343				'type'=>'error'));
 344
 345			return array();
 346		}
 347
 348		if (DEBUG_ENABLED)
 349			debug_log('%s search PREPARE.',16,0,__FILE__,__LINE__,__METHOD__,$query['scope']);
 350
 351		if ($debug)
 352			debug_dump(array('query'=>$query,'server'=>$this->getIndex(),'con'=>$this->connect($method)));
 353
 354		$resource = $this->connect($method,$debug);
 355
 356		switch ($query['scope']) {
 357			case 'base':
 358				$search = @ldap_read($resource,$query['base'],$query['filter'],$query['attrs'],$attrs_only,$query['size_limit'],$query['time_limit'],$query['deref']);
 359				break;
 360
 361			case 'one':
 362				$search = @ldap_list($resource,$query['base'],$query['filter'],$query['attrs'],$attrs_only,$query['size_limit'],$query['time_limit'],$query['deref']);
 363				break;
 364
 365			case 'sub':
 366			default:
 367				$search = @ldap_search($resource,$query['base'],$query['filter'],$query['attrs'],$attrs_only,$query['size_limit'],$query['time_limit'],$query['deref']);
 368				break;
 369		}
 370
 371		if ($debug)
 372			debug_dump(array('method'=>$method,'search'=>$search,'error'=>$this->getErrorMessage()));
 373
 374		if (DEBUG_ENABLED)
 375			debug_log('Search scope [%s] base [%s] filter [%s] attrs [%s] COMPLETE (%s).',16,0,__FILE__,__LINE__,__METHOD__,
 376				$query['scope'],$query['base'],$query['filter'],$query['attrs'],is_null($search));
 377
 378		if (! $search)
 379			return array();
 380
 381		$return = array();
 382
 383		# Get the first entry identifier
 384		if ($entries = ldap_get_entries($resource,$search)) {
 385			# Remove the count
 386			if (isset($entries['count']))
 387				unset($entries['count']);
 388
 389			# Iterate over the entries
 390			foreach ($entries as $a => $entry) {
 391				if (! isset($entry['dn']))
 392					debug_dump_backtrace('No DN?',1);
 393
 394				# Remove the none entry references.
 395				if (! is_array($entry)) {
 396					unset($entries[$a]);
 397					continue;
 398				}
 399
 400				$dn = $entry['dn'];
 401				unset($entry['dn']);
 402
 403				# Iterate over the attributes
 404				foreach ($entry as $b => $attrs) {
 405					# Remove the none entry references.
 406					if (! is_array($attrs)) {
 407						unset($entry[$b]);
 408						continue;
 409					}
 410
 411					# Remove the count
 412					if (isset($entry[$b]['count']))
 413						unset($entry[$b]['count']);
 414				}
 415
 416				# Our queries always include the DN (the only value not an array).
 417				$entry['dn'] = $dn;
 418				$return[$dn] = $entry;
 419			}
 420
 421			# Sort our results
 422			foreach ($return as $key=> $values)
 423				ksort($return[$key]);
 424		}
 425
 426		if (DEBUG_ENABLED)
 427			debug_log('Returning (%s)',17,0,__FILE__,__LINE__,__METHOD__,$return);
 428
 429		return $return;
 430	}
 431
 432	/**
 433	 * Get the last error string
 434	 *
 435	 * @param string Which connection method resource to use
 436	 */
 437	public function getErrorMessage($method=null) {
 438		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 439			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 440
 441		return ldap_error($this->connect($method));
 442	}
 443
 444	/**
 445	 * Get the last error number
 446	 *
 447	 * @param string Which connection method resource to use
 448	 */
 449	public function getErrorNum($method=null) {
 450		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 451			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 452
 453		return ldap_errno($this->connect($method));
 454	}
 455
 456	/**
 457	 * Additional functions
 458	 */
 459	/**
 460	 * Get a user ID
 461	 *
 462	 * @param string Which connection method resource to use
 463	 */
 464	public function getLoginID($user,$method=null) {
 465		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 466			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 467
 468		$query['filter'] = sprintf('(&(%s=%s)%s)',
 469			$this->getValue('login','attr'),$user,
 470			$this->getLoginClass() ? sprintf('(objectclass=%s)',join(')(objectclass=',$this->getLoginClass())) : '');
 471		$query['attrs'] = array('dn');
 472
 473		$result = array();
 474		foreach ($this->getLoginBaseDN() as $base) {
 475			$query['base'] = $base;
 476			$result = $this->query($query,$method);
 477
 478			if (count($result) == 1)
 479				break;
 480		}
 481
 482		if (count($result) != 1)
 483			return null;
 484
 485		$detail = array_shift($result);
 486
 487		if (! isset($detail['dn']))
 488			die('ERROR: DN missing?');
 489		else
 490			return $detail['dn'];
 491	}
 492
 493	/**
 494	 * Return the login base DNs
 495	 * If no login base DNs are defined, then the LDAP server Base DNs are used.
 496	 */
 497	private function getLoginBaseDN() {
 498		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 499			debug_log('Entered (%%)',17,1,__FILE__,__LINE__,__METHOD__,$fargs);
 500
 501		if ($this->getValue('login','base'))
 502			return $this->getValue('login','base');
 503		else
 504			return $this->getBaseDN();
 505	}
 506
 507	/**
 508	 * Return the login classes that a user must have to login
 509	 */
 510	private function getLoginClass() {
 511		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 512			debug_log('Entered (%%)',17,1,__FILE__,__LINE__,__METHOD__,$fargs);
 513
 514		return $this->getValue('login','class');
 515	}
 516
 517	/**
 518	 * Return if anonymous bind is allowed in the configuration
 519	 */
 520	public function isAnonBindAllowed() {
 521		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 522			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 523
 524		return $this->getValue('login','anon_bind');
 525	}
 526
 527	/**
 528	 * Fetches whether TLS has been configured for use with a certain server.
 529	 *
 530	 * Users may configure phpLDAPadmin to use TLS in config,php thus:
 531	 * <code>
 532	 *	$servers->setValue('server','tls',true|false);
 533	 * </code>
 534	 *
 535	 * @return boolean
 536	 */
 537	private function isTLSEnabled() {
 538		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 539			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 540
 541		if ($this->getValue('server','tls') && ! function_exists('ldap_start_tls')) {
 542				error(_('TLS has been enabled in your config, but your PHP install does not support TLS. TLS will be disabled.'),'warn');
 543			return false;
 544
 545		} else
 546			return $this->getValue('server','tls');
 547	}
 548
 549	/**
 550	 * If TLS is configured, then start it
 551	 */
 552	private function startTLS($resource) {
 553		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 554			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 555
 556		if (! $this->getValue('server','tls') || (function_exists('ldap_start_tls') && ! @ldap_start_tls($resource))) {
 557			system_message(array(
 558				'title'=>sprintf('%s (%s)',_('Could not start TLS.'),$this->getName()),
 559				'body'=>sprintf('<b>%s</b>: %s',_('Error'),_('Could not start TLS. Please check your LDAP server configuration.')),
 560				'type'=>'error'));
 561
 562			return false;
 563
 564		} else
 565			return true;
 566	}
 567
 568	/**
 569	 * Fetches whether SASL has been configured for use with a certain server.
 570	 *
 571	 * Users may configure phpLDAPadmin to use SASL in config,php thus:
 572	 * <code>
 573	 *	$servers->setValue('login','auth_type','sasl');
 574	 * </code>
 575	 *
 576	 * @return boolean
 577	 */
 578	private function isSASLEnabled() {
 579		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 580			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 581
 582		if ($this->getValue('login','auth_type') != 'sasl')
 583			return false;
 584
 585		if (! function_exists('ldap_sasl_bind')) {
 586			error(_('SASL has been enabled in your config, but your PHP install does not support SASL. SASL will be disabled.'),'warn');
 587
 588			return false;
 589		}
 590
 591		# If we get here, SASL must be configured.
 592		return true;
 593	}
 594
 595	/**
 596	 * If SASL is configured, then start it
 597	 * To be able to use SASL, PHP should have been compliled with --with-ldap-sasl=DIR
 598	 *
 599	 * @todo This has not been tested, please let the developers know if this function works as expected.
 600	 */
 601	private function startSASL($resource,$method) {
 602		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 603			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 604
 605		static $CACHE = array();
 606
 607		# We shouldnt be doing SASL binds for anonymous queries?
 608		if ($method == 'anon')
 609			return false;
 610
 611		# At the moment, we have only implemented GSSAPI
 612		if (! in_array(strtolower($this->getValue('sasl','mech')),array('gssapi'))) {
 613			system_message(array(
 614				'title'=>_('SASL Method not implemented'),
 615				'body'=>sprintf('<b>%s</b>: %s %s',_('Error'),$this->getValue('sasl','mech'),_('has not been implemented yet')),
 616				'type'=>'error'));
 617
 618			return false;
 619		}
 620
 621		if (! isset($CACHE['login_dn']))
 622			$CACHE['login_dn'] = is_null($this->getLogin($method)) ? $this->getLogin('user') : $this->getLogin($method);
 623
 624		$CACHE['authz_id'] = '';
 625
 626		/*
 627		# Do we need to rewrite authz_id?
 628		if (! isset($CACHE['authz_id']))
 629			if (! trim($this->getValue('sasl','authz_id')) && strtolower($this->getValue('sasl','mech')) != 'gssapi') {
 630				if (DEBUG_ENABLED)
 631					debug_log('Rewriting bind DN [%s] -> authz_id with regex [%s] and replacement [%s].',9,0,__FILE__,__LINE__,__METHOD__,
 632						$CACHE['login_dn'],
 633						$this->getValue('sasl','authz_id_regex'),
 634						$this->getValue('sasl','authz_id_replacement'));
 635
 636				$CACHE['authz_id'] = @preg_replace($this->getValue('sasl','authz_id_regex'),
 637					$this->getValue('sasl','authz_id_replacement'),$CACHE['login_dn']);
 638
 639				# Invalid regex?
 640				if (is_null($CACHE['authz_id']))
 641					error(sprintf(_('It seems that sasl_authz_id_regex "%s" contains invalid PCRE regular expression. The error is "%s".'),
 642						$this->getValue('sasl','authz_id_regex'),(isset($php_errormsg) ? $php_errormsg : '')),
 643						'error','index.php');
 644
 645				if (DEBUG_ENABLED)
 646					debug_log('Resource [%s], SASL OPTIONS: mech [%s], realm [%s], authz_id [%s], props [%s]',9,0,__FILE__,__LINE__,__METHOD__,
 647						$resource,
 648						$this->getValue('sasl','mech'),
 649						$this->getValue('sasl','realm'),
 650						$CACHE['authz_id'],
 651						$this->getValue('sasl','props'));
 652
 653			} else
 654				$CACHE['authz_id'] = $this->getValue('sasl','authz_id');
 655		*/
 656
 657		# @todo this function is different in PHP5.1 and PHP5.2
 658		return @ldap_sasl_bind($resource,NULL,'',
 659			$this->getValue('sasl','mech'),
 660			$this->getValue('sasl','realm'),
 661			$CACHE['authz_id'],
 662			$this->getValue('sasl','props'));
 663	}
 664
 665	/**
 666	 * Fetches whether PROXY AUTH has been configured for use with a certain server.
 667	 *
 668	 * Users may configure phpLDAPadmin to use PROXY AUTH in config,php thus:
 669	 * <code>
 670	 *	$servers->setValue('login','auth_type','proxy');
 671	 * </code>
 672	 *
 673	 * @return boolean
 674	 */
 675	private function isProxyEnabled() {
 676		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 677			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 678
 679		return $this->getValue('login','auth_type') == 'proxy' ? true : false;
 680	}
 681
 682	/**
 683	 * If PROXY AUTH is configured, then start it
 684	 */
 685	private function startProxy($resource,$method) {
 686		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 687			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 688
 689		$rootdse = $this->getRootDSE();
 690
 691		if (! (isset($rootdse['supportedcontrol']) && in_array('2.16.840.1.113730.3.4.18',$rootdse['supportedcontrol']))) {
 692			system_message(array(
 693				'title'=>sprintf('%s %s',_('Unable to start proxy connection'),$this->getName()),
 694				'body'=>sprintf('<b>%s</b>: %s',_('Error'),_('Your LDAP server doesnt seem to support this control')),
 695				'type'=>'error'));
 696
 697			return false;
 698		}
 699
 700		$filter = '(&';
 701		$dn = '';
 702
 703		$missing = false;
 704		foreach ($this->getValue('proxy','attr') as $attr => $var) {
 705			if (! isset($_SERVER[$var])) {
 706				system_message(array(
 707					'title'=>sprintf('%s %s',_('Unable to start proxy connection'),$this->getName()),
 708					'body'=>sprintf('<b>%s</b>: %s (%s)',_('Error'),_('Attribute doesnt exist'),$var),
 709					'type'=>'error'));
 710
 711				$missing = true;
 712
 713			} else {
 714				if ($attr == 'dn') {
 715					$dn = $var;
 716
 717					break;
 718
 719				} else
 720					$filter .= sprintf('(%s=%s)',$attr,$_SERVER[$var]);
 721			}
 722		}
 723
 724		if ($missing)
 725			return false;
 726
 727		$filter .= ')';
 728
 729		if (! $dn) {
 730			$query['filter'] = $filter;
 731
 732			foreach ($this->getBaseDN() as $base) {
 733				$query['base'] = $base;
 734
 735				if ($search = $this->query($query,$method))
 736					break;
 737			}
 738
 739			if (count($search) != 1) {
 740				system_message(array(
 741					'title'=>sprintf('%s %s',_('Unable to start proxy connection'),$this->getName()),
 742					'body'=>sprintf('<b>%s</b>: %s (%s)',_('Error'),_('Search for DN returned the incorrect number of results'),count($search)),
 743					'type'=>'error'));
 744
 745				return false;
 746			}
 747
 748			$search = array_pop($search);
 749			$dn = $search['dn'];
 750		}
 751
 752		$ctrl = array(
 753			'oid'=>'2.16.840.1.113730.3.4.18',
 754			'value'=>sprintf('dn:%s',$dn),
 755			'iscritical' => true);
 756
 757		if (! ldap_set_option($resource,LDAP_OPT_SERVER_CONTROLS,array($ctrl))) {
 758			system_message(array(
 759				'title'=>sprintf('%s %s',_('Unable to start proxy connection'),$this->getName()),
 760				'body'=>sprintf('<b>%s</b>: %s (%s) for <b>%s</b>',_('Error'),$this->getErrorMessage($method),$this->getErrorNum($method),$method),
 761				'type'=>'error'));
 762
 763			return false;
 764		}
 765
 766		$_SESSION['USER'][$this->index][$method]['proxy'] = blowfish_encrypt($dn);
 767
 768		return true;
 769	}
 770
 771	/**
 772	 * Modify attributes of a DN
 773	 */
 774	public function modify($dn,$attrs,$method=null) {
 775		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 776			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 777
 778		# We need to supress the error here - programming should detect and report it.
 779		return @ldap_mod_replace($this->connect($method),$dn,$attrs);
 780	}
 781
 782	/**
 783	 * Gets the root DN of the specified LDAPServer, or null if it
 784	 * can't find it (ie, the server won't give it to us, or it isnt
 785	 * specified in the configuration file).
 786	 *
 787	 * Tested with OpenLDAP 2.0, Netscape iPlanet, and Novell eDirectory 8.7 (nldap.com)
 788	 * Please report any and all bugs!!
 789	 *
 790	 * Please note: On FC systems, it seems that php_ldap uses /etc/openldap/ldap.conf in
 791	 * the search base if it is blank - so edit that file and comment out the BASE line.
 792	 *
 793	 * @param string Which connection method resource to use
 794	 * @return array dn|null The root DN of the server on success (string) or null on error.
 795	 * @todo Sort the entries, so that they are in the correct DN order.
 796	 */
 797	public function getBaseDN($method=null) {
 798		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 799			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 800
 801		static $CACHE;
 802
 803		$method = $this->getMethod($method);
 804		$result = array();
 805
 806		if (isset($CACHE[$this->index][$method]))
 807			return $CACHE[$this->index][$method];
 808
 809		# If the base is set in the configuration file, then just return that.
 810		if (count($this->getValue('server','base'))) {
 811			if (DEBUG_ENABLED)
 812				debug_log('Return BaseDN from Config [%s]',17,0,__FILE__,__LINE__,__METHOD__,implode('|',$this->getValue('server','base')));
 813
 814			$CACHE[$this->index][$method] = $this->getValue('server','base');
 815
 816		# We need to figure it out.
 817		} else {
 818			if (DEBUG_ENABLED)
 819				debug_log('Connect to LDAP to find BaseDN',80,0,__FILE__,__LINE__,__METHOD__);
 820
 821			# Set this to empty, in case we loop back here looking for the baseDNs
 822			$CACHE[$this->index][$method] = array();
 823
 824			$results = $this->getDNAttrValues('',$method);
 825
 826			if (isset($results['namingcontexts'])) {
 827				if (DEBUG_ENABLED)
 828					debug_log('LDAP Entries:%s',80,0,__FILE__,__LINE__,__METHOD__,implode('|',$results['namingcontexts']));
 829
 830				$result = $results['namingcontexts'];
 831			}
 832
 833			$CACHE[$this->index][$method] = $result;
 834		}
 835
 836		return $CACHE[$this->index][$method];
 837	}
 838
 839	/**
 840	 * Gets whether an entry exists based on its DN. If the entry exists,
 841	 * returns true. Otherwise returns false.
 842	 *
 843	 * @param string The DN of the entry of interest.
 844	 * @param string Which connection method resource to use
 845	 * @return boolean
 846	 */
 847	public function dnExists($dn,$method=null) {
 848		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 849			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 850
 851		$results = $this->getDNAttrValues($dn,$method);
 852
 853		if ($results)
 854			return $results;
 855		else
 856			return false;
 857	}
 858
 859	/**
 860	 * Given a DN string, this returns the top container portion of the string.
 861	 *
 862	 * @param string The DN whose container string to return.
 863	 * @return string The container
 864	 */
 865	public function getContainerTop($dn) {
 866		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 867			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 868
 869		$return = $dn;
 870
 871		foreach ($this->getBaseDN() as $base) {
 872			if (preg_match("/${base}$/i",$dn)) {
 873				$return = $base;
 874				break;
 875			}
 876		}
 877
 878		if (DEBUG_ENABLED)
 879			debug_log('Returning (%s)',17,0,__FILE__,__LINE__,__METHOD__,$return);
 880
 881		return $return;
 882	}
 883
 884	/**
 885	 * Given a DN string and a path like syntax, this returns the parent container portion of the string.
 886	 *
 887	 * @param string The DN whose container string to return.
 888	 * @param string Either '/', '.' or something like '../../<rdn>'
 889	 * @return string The container
 890	 */
 891	public function getContainerPath($dn,$path='..') {
 892		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 893			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 894
 895		$top = $this->getContainerTop($dn);
 896
 897		if ($path[0] == '/') {
 898			$dn = $top;
 899			$path = substr($path,1);
 900
 901		} elseif ($path == '.') {
 902			return $dn;
 903		}
 904
 905		$parenttree = explode('/',$path);
 906
 907		foreach ($parenttree as $key => $value) {
 908			if ($value == '..') {
 909				if ($this->getContainer($dn))
 910					$dn = $this->getContainer($dn);
 911
 912				if ($dn == $top)
 913					break;
 914
 915			} elseif($value)
 916				$dn = sprintf('%s,%s',$value,$dn);
 917
 918			else
 919				break;
 920		}
 921
 922		if (! $dn) {
 923			debug_dump(array(__METHOD__,'dn'=>$dn,'path'=>$path));
 924			debug_dump_backtrace('Container is empty?',1);
 925		}
 926
 927		return $dn;
 928	}
 929
 930	/**
 931	 * Given a DN string, this returns the parent container portion of the string.
 932	 * For example. given 'cn=Manager,dc=example,dc=com', this function returns
 933	 * 'dc=example,dc=com'.
 934	 *
 935	 * @param string The DN whose container string to return.
 936	 * @return string The container
 937	 */
 938	public function getContainer($dn) {
 939		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 940			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 941
 942		$parts = $this->explodeDN($dn);
 943
 944		if (count($parts) <= 1)
 945			$return = null;
 946
 947		else {
 948			$return = $parts[1];
 949
 950			for ($i=2;$i<count($parts);$i++)
 951				$return .= sprintf(',%s',$parts[$i]);
 952		}
 953
 954		if (DEBUG_ENABLED)
 955			debug_log('Returning (%s)',17,0,__FILE__,__LINE__,__METHOD__,$return);
 956
 957		return $return;
 958	}
 959
 960	/**
 961	 * Gets a list of child entries for an entry. Given a DN, this function fetches the list of DNs of
 962	 * child entries one level beneath the parent. For example, for the following tree:
 963	 *
 964	 * <code>
 965	 *	dc=example,dc=com
 966	 *		ou=People
 967	 *			cn=Dave
 968	 *			cn=Fred
 969	 *			cn=Joe
 970	 *		ou=More People
 971	 *			cn=Mark
 972	 *			cn=Bob
 973	 * </code>
 974	 *
 975	 * Calling <code>getContainerContents("ou=people,dc=example,dc=com")</code>
 976	 * would return the following list:
 977	 *
 978	 * <code>
 979	 *	cn=Dave
 980	 *	cn=Fred
 981	 *	cn=Joe
 982	 *	ou=More People
 983	 * </code>
 984	 *
 985	 * @param string The DN of the entry whose children to return.
 986	 * @param string Which connection method resource to use
 987	 * @param int (optional) The maximum number of entries to return.
 988	 *            If unspecified, no limit is applied to the number of entries in the returned.
 989	 * @param string (optional) An LDAP filter to apply when fetching children, example: "(objectClass=inetOrgPerson)"
 990	 * @param constant (optional) The LDAP deref setting to use in the query
 991	 * @return array An array of DN strings listing the immediate children of the specified entry.
 992	 */
 993	public function getContainerContents($dn,$method=null,$size_limit=0,$filter='(objectClass=*)',$deref=LDAP_DEREF_NEVER) {
 994		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
 995			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
 996
 997		$return = array();
 998
 999		$query = array();
1000		$query['base'] = $this->escapeDN($dn);
1001		$query['attrs'] = array('dn');
1002		$query['filter'] = $filter;
1003		$query['deref'] = $deref;
1004		$query['scope'] = 'one';
1005		$query['size_limit'] = $size_limit;
1006		$results = $this->query($query,$method);
1007
1008		if ($results) {
1009			foreach ($results as $index => $entry) {
1010				$child_dn = $entry['dn'];
1011				array_push($return,$child_dn);
1012			}
1013		}
1014
1015		if (DEBUG_ENABLED)
1016			debug_log('Returning (%s)',17,0,__FILE__,__LINE__,__METHOD__,$return);
1017
1018		# Sort the results
1019		asort($return);
1020
1021		return $return;
1022	}
1023
1024	/**
1025	 * Explode a DN into an array of its RDN parts.
1026	 *
1027	 * @param string The DN to explode.
1028	 * @param int (optional) Whether to include attribute names (see http://php.net/ldap_explode_dn for details)
1029	 *
1030	 * @return array An array of RDN parts of this format:
1031	 * <code>
1032	 *	Array
1033	 *		(
1034	 *			[0] => uid=ppratt
1035	 *			[1] => ou=People
1036	 *			[2] => dc=example
1037	 *			[3] => dc=com
1038	 *		)
1039	 * </code>
1040	 *
1041	 * NOTE: When a multivalue RDN is passed to ldap_explode_dn, the results returns with 'value + value';
1042	 */
1043	private function explodeDN($dn,$with_attributes=0) {
1044		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
1045			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
1046
1047		static $CACHE;
1048
1049		if (isset($CACHE['explode'][$dn][$with_attributes])) {
1050			if (DEBUG_ENABLED)
1051				debug_log('Return CACHED result (%s) for (%s)',1,0,__FILE__,__LINE__,__METHOD__,
1052					$CACHE['explode'][$dn][$with_attributes],$dn);
1053
1054			return $CACHE['explode'][$dn][$with_attributes];
1055		}
1056
1057		$dn = addcslashes($dn,'<>+";');
1058
1059		# split the dn
1060		$result[0] = ldap_explode_dn($this->escapeDN($dn),0);
1061		$result[1] = ldap_explode_dn($this->escapeDN($dn),1);
1062		if (! $result[$with_attributes]) {
1063			if (DEBUG_ENABLED)
1064				debug_log('Returning NULL - NO result.',1,0,__FILE__,__LINE__,__METHOD__);
1065
1066			return array();
1067		}
1068
1069		# Remove our count value that ldap_explode_dn returns us.
1070		unset($result[0]['count']);
1071		unset($result[1]['count']);
1072
1073		# Record the forward and reverse entries in the cache.
1074		foreach ($result as $key => $value) {
1075			# translate hex code into ascii for display
1076			$result[$key] = $this->unescapeDN($value);
1077
1078			$CACHE['explode'][implode(',',$result[0])][$key] = $result[$key];
1079			$CACHE['explode'][implode(',',array_reverse($result[0]))][$key] = array_reverse($result[$key]);
1080		}
1081
1082		if (DEBUG_ENABLED)
1083			debug_log('Returning (%s)',17,0,__FILE__,__LINE__,__METHOD__,$result[$with_attributes]);
1084
1085		return $result[$with_attributes];
1086	}
1087
1088	/**
1089	 * Parse a DN and escape any special characters
1090	 */
1091	protected function escapeDN($dn) {
1092		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
1093			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
1094
1095		if (! trim($dn))
1096			return $dn;
1097
1098		# Check if the RDN has a comma and escape it.
1099		while (preg_match('/([^\\\\]),(\s*[^=]*\s*),/',$dn))
1100			$dn = preg_replace('/([^\\\\]),(\s*[^=]*\s*),/','$1\\\\2C$2,',$dn);
1101
1102		$dn = preg_replace('/([^\\\\]),(\s*[^=]*\s*)([^,])$/','$1\\\\2C$2$3',$dn);
1103
1104		if (DEBUG_ENABLED)
1105			debug_log('Returning (%s)',17,0,__FILE__,__LINE__,__METHOD__,$dn);
1106
1107		return $dn;
1108	}
1109
1110	/**
1111	 * Parse a DN and unescape any special characters
1112	 */
1113	private function unescapeDN($dn) {
1114		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
1115			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
1116
1117		if (is_array($dn)) {
1118			$a = array();
1119			foreach ($dn as $key => $rdn)
1120				$a[$key] = preg_replace('/\\\([0-9A-Fa-f]{2})/e',"''.chr(hexdec('\\1')).''",$rdn);
1121
1122			return $a;
1123
1124		} else
1125			return preg_replace('/\\\([0-9A-Fa-f]{2})/e',"''.chr(hexdec('\\1')).''",$dn);
1126	}
1127
1128	public function getRootDSE($method=null) {
1129		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
1130			debug_log('Entered (%%)',17,0,__FILE__,__LINE__,__METHOD__,$fargs);
1131
1132		$query = array();
1133		$query['base'] = '';
1134		$query['scope'] = 'base';
1135		$query['attrs'] = $this->getValue('server','root_dse_attributes');
1136		$query['baseok'] = true;
1137		$results = $this->query($query,$method);
1138
1139		if (is_array($results) && count($results) == 1)
1140			return array_change_key_case(array_pop($results));
1141		else
1142			return array();
1143	}
1144
1145	/** Schema Methods **/
1146	/**
1147	 * This function will query the ldap server and request the subSchemaSubEntry which should be the Schema DN.
1148	 *
1149	 * If we cant connect to the LDAP server, we'll return false.
1150	 * If we can connect but cant get the entry, then we'll return null.
1151	 *
1152	 * @param string Which connection method resource to use
1153	 * @param dn The DN to use to obtain the schema
1154	 * @return array|false Schema if available, null if its not or false if we cant connect.
1155	 */
1156	private function getSchemaDN($method=null,$dn='') {
1157		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
1158			debug_log('Entered (%%)',25,0,__FILE__,__LINE__,__METHOD__,$fargs);
1159
1160		# If we already got the SchemaDN, then return it.
1161		if ($this->_schemaDN)
1162			return $this->_schemaDN;
1163
1164		if (! $this->connect($method))
1165			return false;
1166
1167		$search = @ldap_read($this->connect($method),$dn,'objectclass=*',array('subschemaSubentry'),false,0,10,LDAP_DEREF_NEVER);
1168
1169		if (DEBUG_ENABLED)
1170			debug_log('Search returned (%s)',24,0,__FILE__,__LINE__,__METHOD__,is_resource($search));
1171
1172		# Fix for broken ldap.conf configuration.
1173		if (! $search && ! $dn) {
1174			if (DEBUG_ENABLED)
1175				debug_log('Trying to find the DN for "broken" ldap.conf',80,0,__FILE__,__LINE__,__METHOD__);
1176
1177			if (isset($this->_baseDN)) {
1178				foreach ($this->_baseDN as $base) {
1179					$search = @ldap_read($this->connect($method),$base,'objectclass=*',array('subschemaSubentry'),false,0,10,LDAP_DEREF_NEVER);
1180
1181					if (DEBUG_ENABLED)
1182						debug_log('Search returned (%s) for base (%s)',24,0,__FILE__,__LINE__,__METHOD__,
1183							is_resource($search),$base);
1184
1185					if ($search)
1186						break;
1187				}
1188			}
1189		}
1190
1191		if (! $search)
1192			return null;
1193
1194		if (! @ldap_count_entries($this->connect($method),$search)) {
1195			if (DEBUG_ENABLED)
1196				debug_log('Search returned 0 entries. Returning NULL',25,0,__FILE__,__LINE__,__METHOD__);
1197
1198			return null;
1199		}
1200
1201		$entries = @ldap_get_entries($this->connect($method),$search);
1202
1203		if (DEBUG_ENABLED)
1204			debug_log('Search returned [%s]',24,0,__FILE__,__LINE__,__METHOD__,$entries);
1205
1206		if (! $entries || ! is_array($entries))
1207			return null;
1208
1209		$entry = isset($entries[0]) ? $entries[0] : false;
1210		if (! $entry) {
1211			if (DEBUG_ENABLED)
1212				debug_log('Entry is false, Returning NULL',80,0,__FILE__,__LINE__,__METHOD__);
1213
1214			return null;
1215		}
1216
1217		$sub_schema_sub_entry = isset($entry[0]) ? $entry[0] : false;
1218		if (! $sub_schema_sub_entry) {
1219			if (DEBUG_ENABLED)
1220				debug_log('Sub Entry is false, Returning NULL',80,0,__FILE__,__LINE__,__METHOD__);
1221
1222			return null;
1223		}
1224
1225		$this->_schemaDN = isset($entry[$sub_schema_sub_entry][0]) ? $entry[$sub_schema_sub_entry][0] : false;
1226
1227		if (DEBUG_ENABLED)
1228			debug_log('Returning (%s)',25,0,__FILE__,__LINE__,__METHOD__,$this->_schemaDN);
1229
1230		return $this->_schemaDN;
1231	}
1232
1233	/**
1234	 * Fetches the raw schema array for the subschemaSubentry of the server. Note,
1235	 * this function has grown many hairs to accomodate more LDAP servers. It is
1236	 * needfully complicated as it now supports many popular LDAP servers that
1237	 * don't necessarily expose their schema "the right way".
1238	 *
1239	 * Please note: On FC systems, it seems that php_ldap uses /etc/openldap/ldap.conf in
1240	 * the search base if it is blank - so edit that file and comment out the BASE line.
1241	 *
1242	 * @param string Which connection method resource to use
1243	 * @param string A string indicating which type of schema to
1244	 *		fetch. Five valid values: 'objectclasses', 'attributetypes',
1245	 *		'ldapsyntaxes', 'matchingruleuse', or 'matchingrules'.
1246	 *		Case insensitive.
1247	 * @param dn (optional) This paremeter is the DN of the entry whose schema you
1248	 * 		would like to fetch. Entries have the option of specifying
1249	 * 		their own subschemaSubentry that points to the DN of the system
1250	 * 		schema entry which applies to this attribute. If unspecified,
1251	 *		this will try to retrieve the schema from the RootDSE subschemaSubentry.
1252	 *		Failing that, we use some commonly known schema DNs. Default
1253	 *		value is the Root DSE DN (zero-length string)
1254	 * @return array an array of strings of this form:
1255	 *	Array (
1256	 *		[0] => "(1.3.6.1.4.1.7165.1.2.2.4 NAME 'gidPool' DESC 'Pool ...
1257	 *		[1] => "(1.3.6.1.4.1.7165.2.2.3 NAME 'sambaAccount' DESC 'Sa ...
1258	 *	etc.
1259	 */
1260	private function getRawSchema($method,$schema_to_fetch,$dn='') {
1261		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
1262			debug_log('Entered (%%)',25,0,__FILE__,__LINE__,__METHOD__,$fargs);
1263
1264		$valid_schema_to_fetch = array('objectclasses','attributetypes','ldapsyntaxes','matchingrules','matchingruleuse');
1265
1266		if (! $this->connect($method) || $this->noconnect)
1267			return false;
1268
1269		# error checking
1270		$schema_to_fetch = strtolower($schema_to_fetch);
1271
1272		if (! is_null($this->_schema_entries) && isset($this->_schema_entries[$schema_to_fetch])) {
1273			$schema = $this->_schema_entries[$schema_to_fetch];
1274
1275			if (DEBUG_ENABLED)
1276				debug_log('Returning CACHED (%s)',25,0,__FILE__,__LINE__,__METHOD__,$schema);
1277
1278			return $schema;
1279		}
1280
1281		# This error message is not localized as only developers should ever see it
1282		if (! in_array($schema_to_fetch,$valid_schema_to_fetch))
1283			error(sprintf('Bad parameter provided to function to %s::getRawSchema(). "%s" is not valid for the schema_to_fetch parameter.',
1284					get_class($this),$schema_to_fetch),'error','index.php');
1285
1286		# Try to get the schema DN from the specified entry.
1287		$schema_dn = $this->getSchemaDN($method,$dn);
1288
1289		# Do we need to try again with the Root DSE?
1290		if (! $schema_dn && trim($dn))
1291			$schema_dn = $this->getSchemaDN($method,'');
1292
1293		# Store the eventual schema retrieval in $schema_search
1294		$schema_search = null;
1295
1296		if ($schema_dn) {
1297			if (DEBUG_ENABLED)
1298				debug_log('Using Schema DN (%s)',24,0,__FILE__,__LINE__,__METHOD__,$schema_dn);
1299
1300			foreach (array('(objectClass=*)','(objectClass=subschema)') as $schema_filter) {
1301				if (DEBUG_ENABLED)
1302					debug_log('Looking for schema with Filter (%s)',24,0,__FILE__,__LINE__,__METHOD__,$schema_filter);
1303
1304				$schema_search = @ldap_read($this->connect($method),$schema_dn,$schema_filter,array($schema_to_fetch),false,0,10,LDAP_DEREF_NEVER);
1305
1306				if (is_null($schema_search))
1307					continue;
1308
1309				$schema_entries = @ldap_get_entries($this->connect($method),$schema_search);
1310
1311				if (DEBUG_ENABLED)
1312					debug_log('Search returned [%s]',24,0,__FILE__,__LINE__,__METHOD__,$schema_entries);
1313
1314				if (is_array($schema_entries) && isset($schema_entries['count']) && $schema_entries['count']) {
1315					if (DEBUG_ENABLED)
1316						debug_log('Found schema with (DN:%s) (FILTER:%s) (ATTR:%s)',24,0,__FILE__,__LINE__,__METHOD__,
1317							$schema_dn,$schema_filter,$schema_to_fetch);
1318
1319					break;
1320				}
1321
1322				if (DEBUG_ENABLED)
1323					debug_log('Didnt find schema with filter (%s)',24,0,__FILE__,__LINE__,__METHOD__,$schema_filter);
1324
1325				unset($schema_entries);
1326				$schema_search = null;
1327			}
1328		}
1329
1330		/* Second chance: If the DN or Root DSE didn't give us the subschemaSubentry, ie $schema_search
1331		 * is still null, use some common subSchemaSubentry DNs as a work-around. */
1332		if (is_null($schema_search)) {
1333			if (DEBUG_ENABLED)
1334				debug_log('Attempting work-arounds for "broken" LDAP servers...',24,0,__FILE__,__LINE__,__METHOD__);
1335
1336			foreach ($this->getBaseDN() as $base) {
1337				$ldap['W2K3 AD'][expand_dn_with_base($base,'cn=Aggregate,cn=Schema,cn=configuration,')] = '(objectClass=*)';
1338				$ldap['W2K AD'][expand_dn_with_base($base,'cn=Schema,cn=configuration,')] = '(objectClass=*)';
1339				$ldap['W2K AD'][expand_dn_with_base($base,'cn=Schema,ou=Admin,')] = '(objectClass=*)';
1340			}
1341
1342			# OpenLDAP and Novell
1343			$ldap['OpenLDAP']['cn=subschema'] = '(objectClass=*)';
1344
1345			foreach ($ldap as $ldap_server_name => $ldap_options) {
1346				foreach ($ldap_options as $ldap_dn => $ldap_filter) {
1347					if (DEBUG_ENABLED)
1348						debug_log('Attempting [%s] (%s) (%s)<BR>',24,0,__FILE__,__LINE__,__METHOD__,
1349							$ldap_server_name,$ldap_dn,$ldap_filter);
1350
1351					$schema_search = @ldap_read($this->connect($method),$ldap_dn,$ldap_filter,array($schema_to_fetch),false,0,10,LDAP_DEREF_NEVER);
1352					if (is_null($schema_search))
1353						continue;
1354
1355					$schema_entries = @ldap_get_entries($this->connect($method),$schema_search);
1356
1357					if (DEBUG_ENABLED)
1358						debug_log('Search returned [%s]',24,0,__FILE__,__LINE__,__METHOD__,$schema_entries);
1359
1360					if ($schema_entries && isset($schema_entries[0][$schema_to_fetch])) {
1361						if (DEBUG_ENABLED)
1362							debug_log('Found schema with filter of (%s)',24,0,__FILE__,__LINE__,__METHOD__,$ldap_filter);
1363
1364						break;
1365					}
1366
1367					if (DEBUG_ENABLED)
1368						debug_log('Didnt find schema with filter (%s)',24,0,__FILE__,__LINE__,__METHOD__,$ldap_filter);
1369
1370					unset($schema_entries);
1371					$schema_search = null;
1372				}
1373				if ($schema_search)
1374					break;
1375			}
1376		}
1377
1378		# Option 3: try cn=config
1379		$olc_schema = 'olc'.$schema_to_fetch;
1380		$olc_schema_found = false;
1381		if (is_null($schema_search)) {
1382			if (DEBUG_ENABLED)
1383				debug_log('Attempting cn=config work-around...',24,0,__FILE__,__LINE__,__METHOD__);
1384
1385			$ldap_dn = 'cn=schema,cn=config';
1386			$ldap_filter = '(objectClass=*)';
1387
1388			$schema_search = @ldap_search($this->connect($method),$ldap_dn,$ldap_filter,array($olc_schema),false,0,10,LDAP_DEREF_NEVER);
1389
1390			if (! is_null($schema_search)) {
1391				$schema_entries = @ldap_get_entries($this->connect($method),$schema_search);
1392
1393				if (DEBUG_ENABLED)
1394					debug_log('Search returned [%s]',24,0,__FILE__,__LINE__,__METHOD__,$schema_entries);
1395
1396				if ($schema_entries) {
1397					if (DEBUG_ENABLED)
1398						debug_log('Found schema with filter of (%s) and attribute filter (%s)',24,0,__FILE__,__LINE__,__METHOD__,$ldap_filter,$olc_schema);
1399
1400					$olc_schema_found = true;
1401
1402				} else {
1403					if (DEBUG_ENABLED)
1404						debug_log('Didnt find schema with filter (%s) and attribute filter (%s)',24,0,__FILE__,__LINE__,__METHOD__,$ldap_filter,$olc_schema);
1405
1406					unset($schema_entries);
1407					$schema_search = null;
1408				}
1409			}
1410		}
1411
1412		if (is_null($schema_search)) {
1413			/* Still cant find the schema, try with the RootDSE
1414			 * Attempt to pull schema from Root DSE with scope "base", or
1415			 * Attempt to pull schema from Root DSE with scope "one" (work-around for Isode M-Vault X.500/LDAP) */
1416			foreach (array('base','one') as $ldap_scope) {
1417				if (DEBUG_ENABLED)
1418					debug_log('Attempting to find schema with scope (%s), filter (objectClass=*) and a blank base.',24,0,__FILE__,__LINE__,__METHOD__,
1419						$ldap_scope);
1420
1421				switch ($ldap_scope) {
1422					case 'base':
1423						$schema_search = @ldap_read($this->connect($method),'','(objectClass=*)',array($schema_to_fetch),false,0,10,LDAP_DEREF_NEVER);
1424						break;
1425
1426					case 'one':
1427						$schema_search = @ldap_list($this->connect($method),'','(objectClass=*)',array($schema_to_fetch),false,0,10,LDAP_DEREF_NEVER);
1428						break;
1429				}
1430
1431				if (is_null($schema_search))
1432					continue;
1433
1434				$schema_entries = @ldap_get_entries($this->connect($method),$schema_search);
1435				if (DEBUG_ENABLED)
1436					debug_log('Search returned [%s]',24,0,__FILE__,__LINE__,__METHOD__,$schema_entries);
1437
1438				if ($schema_entries && isset($schema_entries[0][$schema_to_fetch])) {
1439					if (DEBUG_ENABLED)
1440						debug_log('Found schema with filter of (%s)',24,0,__FILE__,__LINE__,__METHOD__,'(objectClass=*)');
1441
1442					break;
1443				}
1444
1445				if (DEBUG_ENABLED)
1446					debug_log('Didnt find schema with filter (%s)',24,0,__FILE__,__LINE__,__METHOD__,'(objectClass=*)');
1447
1448				unset($schema_entries);
1449				$schema_search = null;
1450			}
1451		}
1452
1453		$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.';
1454		$schema_error_message_array = array('objectclasses','attributetypes');
1455
1456		# Shall we just give up?
1457		if (is_null($schema_search)) {
1458			# We need to have objectclasses and attribues, so display an error, asking the user to get us this information.
1459			if (in_array($schema_to_fetch,$schema_error_message_array))
1460				system_message(array(
1461					'title'=>sprintf('%s (%s)',_('Our attempts to find your SCHEMA have failed'),$schema_to_fetch),
1462					'body'=>sprintf('<b>%s</b>: %s',_('Error'),$schema_error_message),
1463					'type'=>'error'));
1464			else
1465				if (DEBUG_ENABLED)
1466					debug_log('Returning because schema_search is NULL ()',25,0,__FILE__,__LINE__,__METHOD__);
1467
1468			# We'll set this, so if we return here our cache will return the known false.
1469			$this->_schema_entries[$schema_to_fetch] = false;
1470			return false;
1471		}
1472
1473		if (! $schema_entries) {
1474			$return = false;
1475			if (DEBUG_ENABLED)
1476				debug_log('Returning false since ldap_get_entries() returned false.',25,0,__FILE__,__LINE__,__METHOD__,$return);
1477
1478			return $return;
1479		}
1480
1481		if ($olc_schema_found) {
1482			unset ($schema_entries['count']);
1483
1484			foreach ($schema_entries as $entry) {
1485				if (isset($entry[$olc_schema])) {
1486					unset($entry[$olc_schema]['count']);
1487
1488					foreach ($entry[$olc_schema] as $schema_definition)
1489						/* Schema definitions in child nodes prefix the schema entries with "{n}"
1490						  the preg_replace call strips out this prefix. */
1491						$schema[] = preg_replace('/^\{\d*\}\(/','(',$schema_definition);
1492				}
1493			}
1494
1495			if (isset($schema)) {
1496				$this->_schema_entries[$olc_schema] = $schema;
1497
1498				if (DEBUG_ENABLED)
1499					debug_log('Returning (%s)',25,0,__FILE__,__LINE__,__METHOD__,$schema);
1500
1501				return $schema;
1502
1503			} else
1504				return null;
1505		}
1506
1507		if (! isset($schema_entries[0][$schema_to_fetch])) {
1508			if (in_array($schema_to_fetch,$schema_error_message_array)) {
1509				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>',
1510					$schema_to_fetch,gettype($schema_search),$schema_error_message,serialize($schema_entries)),'error','index.php');
1511
1512			} else {
1513				$return = false;
1514
1515				if (DEBUG_ENABLED)
1516					debug_log('Returning because (%s) isnt in the schema array. (%s)',25,0,__FILE__,__LINE__,__METHOD__,$schema_to_fetch,$return);
1517
1518				return $return;
1519			}
1520		}
1521
1522		/* Make a nice array of this form:
1523			Array (
1524				[0] => "(1.3.6.1.4.1.7165.1.2.2.4 NAME 'gidPool' DESC 'Pool ...)"
1525				[1] => "(1.3.6.1.4.1.7165.2.2.3 NAME 'sambaAccount' DESC 'Sa ...)"
1526			etc.) */
1527
1528		$schema = $schema_entries[0][$schema_to_fetch];
1529		unset($schema['count']);
1530		$this->_schema_entries[$schema_to_fetch] = $schema;
1531
1532		if (DEBUG_ENABLED)
1533			debug_log('Returning (%s)',25,0,__FILE__,__LINE__,__METHOD__,$schema);
1534
1535		return $schema;
1536	}
1537
1538	/**
1539	 * Gets a single ObjectClass object specified by name.
1540	 *
1541	 * @param string $oclass_name The name of the objectClass to fetch.
1542	 * @param string $dn (optional) It is easier to fetch schema if a DN is provided
1543	 *               which defines the subschemaSubEntry attribute (all entries should).
1544	 *
1545	 * @return ObjectClass The specified ObjectClass object or false on error.
1546	 *
1547	 * @see ObjectClass
1548	 * @see SchemaObjectClasses
1549	 */
1550	public function getSchemaObjectClass($oclass_name,$method=null,$dn='') {
1551		if (DEBUG_ENABLED && (($fargs=func_get_args())||$fargs='NOARGS'))
1552			debug_log('Entered (%%)',25,0,__FILE__,__LINE__,__METHOD__,$fargs);
1553
1554		$oclass_name = strtolower($oclass_name);
1555		$socs = $this->SchemaObjectClasses($method,$dn);
1556
1557		# Default return…

Large files files are truncated, but you can click here to view the full file