<?php defined('SYSPATH') or die('No direct access allowed.');

/**
 * This class takes care of communicating with LDAP
 *
 * @package    Kohana/Database
 * @category   Drivers
 * @author     Deon George
 * @copyright  (c) 2013 phpLDAPadmin Development Team
 * @license    http://dev.phpldapadmin.org/license.html
 */
abstract class Kohana_Database_LDAP extends Kohana_LDAP {
	// LDAP doesnt use an identifier
	protected $_identifier = '';

	/**
	 * @defunct This required abstruct function is defunct for LDAP
	 */
	public function begin($mode = NULL) { throw HTTP_Exception::factory(501,'We shouldnt be here: :method',array(':method'=>__METHOD__)); }
	/**
	 * @defunct This required abstruct function is defunct for LDAP
	 */
	public function commit() { throw HTTP_Exception::factory(501,'We shouldnt be here: :method',array(':method'=>__METHOD__)); }
	/**
	 * @defunct This required abstruct function is defunct for LDAP
	 */
	public function list_tables($like = NULL) { throw HTTP_Exception::factory(501,'We shouldnt be here: :method',array(':method'=>__METHOD__)); }
	/**
	 * @defunct This required abstruct function is defunct for LDAP
	 */
	public function query($type, $sql, $as_object = FALSE, array $params = NULL) { throw HTTP_Exception::factory(501,'We shouldnt be here: :method',array(':method'=>__METHOD__)); }
	/**
	 * @defunct This required abstruct function is defunct for LDAP
	 */
	public function rollback() { throw HTTP_Exception::factory(501,'We shouldnt be here: :method',array(':method'=>__METHOD__)); }
	/**
	 * @defunct This required abstruct function is defunct for LDAP
	 */
	public function set_charset($charset) { throw HTTP_Exception::factory(501,'We shouldnt be here: :method',array(':method'=>__METHOD__)); }

	/** REQUIRED ABSTRACT FUNCTIONS **/
	public function escape($value) { return $value;}

	/**
	 * @override We provide the columns that are in all LDAP objects
	 */
	public function list_columns($table,$like=NULL,$add_prefix=TRUE) {
		return array('dn'=>array(),'objectclass'=>array());
	}

	/**
	 * @override We override this parent function, since LDAP doesnt quote columns
	 */
	public function quote_column($column) {
		return $column;
	}

	/** LDAP **/

	/**
	 * Bind to the LDAP server with the creditials
	 *
	 * If we are successful, we return TRUE, if not FALSE
	 *
	 * @return boolean TRUE|FALSE
	 */
	private function _bind($u,$p) {
		/*
		// @todo To implement
		// If SASL has been configured for binding, then start it now.
		if ($this->isSASLEnabled())
			$br = $this->startSASL($this->_r,$method);

		// Normal bind...
		else
		*/

		if (Kohana::$profiling)
			$benchmark = Profiler::start("Database Bind ({$this->_instance})", $this->_instance);

		try {
			$br = ldap_bind($this->_connection,$u,$p);

		} catch (Exception $e) {
			// This benchmark is worthless
			if (isset($benchmark))
				Profiler::delete($benchmark);

			return FALSE;
		}

		if (! $br)
			return FALSE;

		$this->_connected = TRUE;

		/*
		// @todo To implement
		# If this is a proxy session, we need to switch to the proxy user
		if ($this->isProxyEnabled() && $bind['id'] && $method != 'anon')
			if (! $this->startProxy($this->_r,$method)) {
				$CACHE[$this->index][$method] = null;
			}
		*/

		/*
		// @todo To implement
		if (function_exists('run_hook'))
			run_hook('post_connect',array('server_id'=>$this->index,'method'=>$method,'id'=>$bind['id']));
		*/

		// Get our Schema.
		if (isset($benchmark)) {
			Profiler::stop($benchmark);
			$benchmark = Profiler::start("Schema Retrieve ({$this->_instance})", $this->_instance);
		}

		if ($this->_instance == 'auth') {
			$this->getSchema();

		}

		if (isset($benchmark))
			Profiler::stop($benchmark);

		return $br;
	}

	/**
	 * Bind to the LDAP server
	 *
	 * If we have been passed a login_attr that is not DN, we'll try and find the
	 * DN to bind with.
	 *
	 * @param string User attribute to connect with, or blank for anonymous
	 * @param string Bind password to use with a DN or blank for anonymous
	 * @return mixed $this|FALSE
	 */
	public function bind($user,$pass) {
		// If we are already connected, no need to re-bind.
		if ($this->_connected)
			return $this;

		// Make sure we are connected.
		$this->_connection OR $this->connect();

		// Do we need to do an search to find the DN
		if (! empty($this->_config['login_attr']) AND strtoupper($this->_config['login_attr']) != 'DN') {
			// Do we need to authenticate for this search?
			// Extract the connection parameters, adding required variabels
			extract($this->_config['connection'] + array(
				'username'   => '',
				'password'   => '',
				'hostname'   => '',
				'port'      => '',
			));

			// Prevent this information from showing up in traces
			unset($this->_config['connection']['username'], $this->_config['connection']['password']);

			// Sanity check
			if ($this->_instance == 'auth')
				throw new Kohana_Exception('We shouldnt be authing an auth');

			$config = Arr::merge($this->_config,array(
				'login_attr'=>'DN',
				'connection'=>array(
					'hostname'=>$hostname,
					'port'=>$port,
				),
			));

			try {
				$x = LDAP::factory('auth',NULL,$config);

				// Our Auth Bind credentials are wrong
				if (! $x->bind($username,$password))
					return FALSE;

				$u = $x->search(NULL)
					->scope('sub')
					->where($this->_config['login_attr'],'=',$user)
					->execute();

				if (! $u)
					return FALSE;

			} catch (Exception $e) {
				// If we are a command line, we can just print the error
				echo _('Unable to bind to LDAP server with CONFIG settings, please check them.');
				echo _('The error message is').': '.$e->getMessage();
				die();
			}

			foreach ($u as $dn => $leaf)
				if ($this->_bind($dn,$pass))
					return ORM::factory('LDAP',$dn);

			// We didnt find an AUTH DN to bind with
			return FALSE;
		}

		// Bind
		if ($this->_bind($user,$pass))
			return $this;
		else
			return FALSE;
	}

	/**
	 * Create a connection to an LDAP server
	 */
	public function connect() {
		if ($this->_connection)
			return;

		// Extract the connection parameters, adding required variabels
		extract($this->_config['connection'] + array(
			'hostname'   => '',
			'port'       => '',
		));

		/*
		// @todo To implement
		if (function_exists('run_hook'))
			run_hook('pre_connect',array('server_id'=>$this->index,'method'=>$method));
		*/

		// Benchmark this connection for the current instance
		if (Kohana::$profiling)
			$benchmark = Profiler::start("Database Connect ({$this->_instance})", $this->_instance);

		$r = (! empty($this->_config['port'])) ? ldap_connect($hostname,$port) : ldap_connect($hostname);

		if (! is_resource($r)) {
			// This benchmark is worthless
			if (isset($benchmark))
				Profiler::delete($benchmark);

			throw HTTP_Exception::factory(501,'UNHANDLED, $r is not a resource');
		}

		// Go with LDAP version 3 if possible (needed for renaming and Novell schema fetching)
		ldap_set_option($r,LDAP_OPT_PROTOCOL_VERSION,3);

		/* Disabling this makes it possible to browse the tree for Active Directory, and seems
		 * to not affect other LDAP servers (tested with OpenLDAP) as phpLDAPadmin explicitly
		 * specifies deref behavior for each ldap_search operation. */
		ldap_set_option($r,LDAP_OPT_REFERRALS,0);

		/*
		// @todo To implement
		# Try to fire up TLS is specified in the config
		if ($this->isTLSEnabled())
			$this->startTLS($this->_connection);
		*/

		if (isset($benchmark))
			Profiler::stop($benchmark);

		$this->_connection = $r;
	}

	public function disconnect() {
		try {
			// Database is assumed disconnected
			$status = TRUE;

			if (is_resource($this->_connection)) {
				if ($status = ldap_unbind($this->_connection)) {
					// Clear the connection
					$this->_connection = NULL;

					// Clear the instance
					parent::disconnect();
				}
			}

		} catch (Exception $e) {
			// Database is probably not disconnected
			$status = ! is_resource($this->_connection);
		}

		return $status;
	}

	public function getSchema() {
		// Make sure our login_attr is DN
		if ($this->_instance == 'schema' AND $this->_config['login_attr'] != 'DN')
			$this->_config['login_attr'] = 'DN';

		$x = LDAP::factory('schema');

		try {
			// @todo We should bind as specific shema DN, logged in User or anonymous.
			if ($x->bind((isset($this->_config['schema']['dn']) ? $this->_config['schema']['dn'] : FALSE),(isset($this->_config['schema']['password']) ? $this->_config['schema']['password'] : FALSE))) {
				$u = $x->search(array(''))
					->scope('base')
					->execute();

				if (! $u OR ! isset($u[''][0]['subschemasubentry'][0]))
					throw new Kohana_Exception('Couldnt find schema?');

				$x->setSchema(ORM::factory('LDAP_Schema',$u[''][0]['subschemasubentry'][0]));
			}

		} catch (Exception $e) {
			// If we are a command line, we can just print the error
			echo _('Unable to retrieve the SCHEMA from the LDAP server.');
			echo _('The error message is').': '.$e->getMessage();
			die();
		}
	}

	public function search($base=array()) {
		return new Database_LDAP_Search($this,$base);
	}
}
?>