<?php

/**
 * This is an authentication backend that uses a file to manage passwords.
 *
 * The backend file must conform to Apache's htdigest format
 * 
 * @package Atmail
 * @subpackage Dav
 * @copyright Copyright (C) 2007-2011 Rooftop Solutions. All rights reserved. (modifications are Copyright (C) 2011 Atmail)
 * @author Evert Pot (http://www.rooftopsolutions.nl/) (modified by Atmail)
 * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
 */
class AUTHMODE
{
	const SQL = 'SQL';
	const LDAP = 'LDAP';
};

class Atmail_Dav_Backend_Auth extends Sabre_DAV_Auth_Backend_AbstractBasic
{

	/**
	 * PDO object
	 * 
	 * @var string 
	 */
	private $pdo;
	
	/**
	* array for tracking provisioned usernames during multihit call
	*
	* @var array
	*/
	private $provisioned;
	/**
	* array for tracking auth usernames during multihit call
	*
	* @var array
	*/
	private $authorized;

	/**
	* var for setting password encryption type
	*
	* @var array
	*/
	private $passwordMode;
	
	private $authMode;
	private $atmailLdap = null;
	
	private $debug = false;

	private $remoteDomainsAllowed;
	private $remoteDomains;
	private $localDomains;
	
	private $useImapFallback;
		
	/**
	 * Creates the backend object. 
	 *
	 * If the filename argument is passed in, it will parse out the specified file fist.
	 * 
	 * @param string $filename 
	 * @return void
	 */

	public function __construct(PDO $pdo)
	{
		$this->pdo = $pdo;
		$this->authorized = array();
		$this->provisioned = array();
		$this->passwordMode = Atmail_Enum::PASSWORD_PLAIN;
		$this->authMode = AUTHMODE::SQL;
		$this->useImapFallback = false;		// enable this to use the imap backend as fallback on password fail (for out-of-sync session tables)
		
		$stmt = $this->pdo->prepare('SELECT keyValue from Config where keyName = "userPasswordEncryptionType"');
		if($stmt)
		{
			$stmt->execute();
			$result = $stmt->fetchAll();
			$result = strtoupper($result[0]['keyValue']);
			switch($result)
			{
				case Atmail_Enum::PASSWORD_PLAIN:
				case Atmail_Enum::PASSWORD_MD5:
				case Atmail_Enum::PASSWORD_MD5_CRYPT:
				case Atmail_Enum::PASSWORD_ENCRYPTED:
				{
					$this->passwordMode = $result;	
				} break;
			}
		}
		$stmt = $this->pdo->prepare('SELECT keyValue from Config where keyName = "authType"');
		if($stmt)
		{
			$stmt->execute();
			$result = $stmt->fetchAll();
			$result = strtoupper($result[0]['keyValue']);
			switch($result)
			{
				case AUTHMODE::SQL:
				case AUTHMODE::LDAP:
				{
					$this->authMode = $result;				
				} break;
			}
		}
		
    	$globalConfig = Zend_Registry::get('config')->global;
		$localDomains = domains::getList();
			
		$this->localDomains = (is_array($localDomains) ? $localDomains : ($localDomains == '' ? array() : array($localDomains)));
		$this->remoteDomains = 
		array_key_exists('remoteDomains',  $globalConfig)
		?
			(
				!is_array( $globalConfig['remoteDomains'])
				?	(
						strpos( $globalConfig['remoteDomains'], "\n") === false
						?	array( $globalConfig['remoteDomains'])
						:	explode("\n",  $globalConfig['remoteDomains'])
					)
				:	 $globalConfig['remoteDomains']
			)
		: array();
		$this->remoteDomainsAllowed = !empty($globalConfig['remoteDomainsOverwrite']); //true if any remote domain allowed, false if allowed list specified

		$this->log("Calendar Auth INIT\n" . 
		"authMode = " . $this->authMode . "\n" .
		"passwordMode = " . $this->passwordMode . "\n" . 
		"local domains = " . print_r($this->localDomains, true) . "\n" .
		"remote domains = " . print_r($this->remoteDomains, true) . "\n" .
		"remote domains allowed = " . ($this->remoteDomainsAllowed ? 'true' : 'false') . "\n");
	}

	public function authenticate(Sabre_DAV_Server $server,$realm) {
		// make sure that the authentication username contains the domain for which it belongs to

		return parent::authenticate( $server, $realm );
    }
    
    private function log($msg)
    {
    	if(!$this->debug)
    		return;

    	return file_put_contents("php://stderr", $msg . "\n"); 
    }
    /**
	 * Validates if the username is enabled
	 * 
	 * @param bool $isExternal
	 * @param string $username 
	 * @return bool 
	 */
    public function checkUserEnabled($isExternal, $username)
    {
    	// check that the username is sane
    	if(strpos($username, '@') === false)
    	{
    		$this->log('WEBDAV : User ' . $username . ' is disabled due to missing domain details.');
    		return false;
    	}
    	
    	list($user, $domain) = explode('@', $username);
    	
		if(!$this->remoteDomainsAllowed)
		{
			if( !in_array($domain, $this->localDomains) && !in_array($domain, $this->remoteDomains) )
			{
				$this->log('WEBDAV : User ' . $username . ' is disabled as remote domain ' . $domain . ' is disabled.');
				return false;
			}
		}
		
    	if($isExternal)
    	{
    		$this->log('WEBDAV : User ' . $username . ' is external');
    		
    		// the external hosts check has already been completed above,
    		// so dont need to check against Domains table for enabled state
    		$sql = 	'
	            SELECT distinct id from Users 
	            join Groups ON Users.Ugroup = GroupName
	            WHERE Account = ? AND (UserStatus = 0 OR UserStatus IS NULL) and Groups.Calendar = 1
	        ';
    	}
    	else
    	{
    		$sql = 	'
	            SELECT distinct id from Users 
	            join Groups ON Users.Ugroup = GroupName 
	            join Domains ON HostName = SUBSTRING_INDEX(Users.Account, "@", -1)
	            WHERE Account = ? AND (UserStatus = 0 OR UserStatus IS NULL) and Groups.Calendar = 1 and Domains.Enable = 1
	        ';
    	}
    	$stmt = $this->pdo->prepare($sql);
		$stmt->execute(array($username));
		$result = $stmt->fetchAll();

		if (!count($result))
		{
			$this->log('WEBDAV : User ' . $username . ' is disabled');
			return false;
		}		
		return true;
    }
    
    /**
	 * Validates if the username is enabled
	 * 
	 * @param string $username 
	 * @return bool 
	 */
    public function checkUserExternal($username)
    {
    	list($user, $domain) = explode('@', $username);
		return !in_array($domain, $this->localDomains);
    }

	private function authSQL($username, $password)
	{
		$isExternal = $this->checkUserExternal($username);
		if(!$this->checkUserEnabled($isExternal, $username))
		{
			$this->log('WEBDAV : Failed login ' . $username);
			return false;
		}

		if($isExternal)
		{
			$stmt = $this->pdo->prepare('SELECT keyValue from Config where keyName = "externalUserPasswordEncryptionType"');
			if($stmt)
			{
				$stmt->execute();
				$result = $stmt->fetchAll();
				$result = strtoupper($result[0]['keyValue']);
				switch($result)
				{
					case Atmail_Enum::PASSWORD_PLAIN:
					case Atmail_Enum::PASSWORD_MD5:
					case Atmail_Enum::PASSWORD_MD5_CRYPT:
					case Atmail_Enum::PASSWORD_ENCRYPTED:
					{
						$externalPasswordMode = $result;	
					} break;
				}
				$this->log('WEBDAV : Setting ' . $externalPasswordMode . ' for external user ' . $username);
				$this->passwordMode = $externalPasswordMode;
			}
			else
			{
				// failed sql passwordMode
				$this->log('WEBDAV : FAILED to fetch external user Password mode. Setting PLAIN for ' . $username);
				$this->passwordMode = Atmail_Enum::PASSWORD_PLAIN;			
			}
		}
				
		$dbPassword = $this->getPassword($username);
		if($dbPassword === false)
		{
			// some sort of error
			return false;	
		}
						
		// test password
		switch($this->passwordMode)
		{
			case Atmail_Enum::PASSWORD_MD5:
			{
				$password = md5($password);
			} break;
			
			case Atmail_Enum::PASSWORD_MD5_CRYPT:
			{
				$password = crypt($password, $dbPassword);
			} break;
		}

		if ($dbPassword != $password)
		{
			$this->log('WEBDAV : Login failure ' . $username);
			if(!$this->useImapFallback)
			{
				return false;
			}
			else
			{
				// DB auth failed, so fall back on IMAP to check for pw change
				$stmt = $this->pdo->prepare('SELECT MailServer, UseSSL FROM UserSettings WHERE Account = ?');
				$stmt->execute(array($username)); $result = $stmt->fetchAll();
				
				if (!count($result))
				{
					return false;
				}
				
				// hit IMAP server each time for Auth
				$host = $result[0]['MailServer'];
				$port = null; // IMAP class decides based on SSL
				$ssl  = $result[0]['UseSSL'] ? 'SSL': false;
				
				$imap = new Zend_Mail_Protocol_Imap($host, $port, $ssl);
				
				pluginCall('calDavImapUsername');
				if (Zend_Registry::isRegistered('calDavImapUsername'))
				{
					if(!$imap->login(Zend_Registry::get('calDavImapUsername'), $password))
					{
						return false;
					}
				}	
				elseif(!$imap->login($username, $password)) 
				{
					return false;
				}
			}
		}
			
		// cache provision results for repeats calls during a hit
		$this->authorized[$username] = $dbPassword;
					
		return true;	
	}
	
	private function authLDAP($username, $password)
	{
		$this->log('WEBDAV : Password mode is PLAIN for ' . $username);
		$this->passwordMode = Atmail_Enum::PASSWORD_PLAIN;
		$isExternal = $this->checkUserExternal($username);
		
		// no provisioning or password cache, query ldap
		if(!isset($this->authorized[$username]) || ( isset($this->authorized[$username]) && $this->authorized[$username] != $password) )
		{
			try
			{
				$atmailLdap = new Atmail_Ldap($username, Zend_Registry::get('config')->dovecot);
				$ldapOptions = $atmailLdap->ldapOptions();
				$ldapOptionsRoot = $atmailLdap->ldapCurrentRootDetails();
		
				$ldap = new Zend_Ldap($ldapOptions);
				$var = $ldap->bind($atmailLdap->bindAuthDn, $password);
			}
			catch (Zend_Ldap_Exception $e)
			{
				switch($e->getCode())
				{
					case Zend_Ldap_Exception::LDAP_INVALID_CREDENTIALS :
					{
						throw new Exception('Username or password invalid.');
					}
					default:
						throw new Exception('LDAP server : ' . $e->getCode() . ' ' . $e->getMessage());
				}
				// no custom handlers, throw general error
				return false;
			}
		}
		
		// see if this user has a usersession entry and if so check
		// the users enable status
		if($this->getPassword($username) !== false && !$this->checkUserEnabled($isExternal, $username))
		{
			$this->log('WEBDAV : Failed login ' . $username);
			return false;
		}

		// cache provision results for repeats calls during a hit
		$this->authorized[$username] = $password;
			
		return true;	
	}
	/**
	 * Validates if the username and password are correct 
	 * 
	 * @param string $username 
	 * @param string $password 
	 * @return bool 
	 */

	public function validateUserPass($username, $password)
	{
		$this->log('WEBDAV : Validating user ' . $username);

		switch($this->authMode)
		{
			case AUTHMODE::SQL:
			{
				$result = $this->authSQL($username, $password);
			} break;
			case AUTHMODE::LDAP:
			{
				$result = $this->authLDAP($username, $password);
			} break;
			default:
				return false;
		}
		if($result)
		{
			$this->provisionUser($username);
			$this->log('WEBDAV : Login success ' . $username);
		}
			
		return $result;	
	}

	/**
	 * Returns any information found for a user. 
	 * Can include plaintext/md5/md5crypt
	 * 
	 * @param string $username 
	 * @return array|null 
	 */

	public function getPassword($username)
	{
		if(!isset($this->authorized[$username]))
		{
			$stmt = $this->pdo->prepare('SELECT password as password FROM UserSession WHERE Account = ?');
			$stmt->execute(array($username));
			$result = $stmt->fetchAll();
	
			if (!count($result))
			{
				return false;
			}
		}
		else
		{
			return $this->authorized[$username];
		}
		
		if($this->passwordMode == Atmail_Enum::PASSWORD_ENCRYPTED)
		{
			return Atmail_Password::hexaes_decrypt($result[0]['password']);
		}
		return $result[0]['password'];
	}

	/**
	 * Sets up database records for a user, if needed 
	 * 
	 * @param string $username 
	 * @return void
	 */

	protected function provisionUser($username)
	{
		
		if(isset($this->provisioned[$username]))
		{
			return true;
		}	
		
		$this->log('WEBDAV : Provisioning user ' . $username);
		
		$principalUri = 'principals/users/' . $username;
		
		// ensure the user has a principal account
		$stmt = $this->pdo->prepare('SELECT email FROM principals WHERE email = ? LIMIT 1');
		$stmt->execute(array($username));

		$result = $stmt->fetchAll();
		$hasPrinicpal = (count($result) != 0);
		
		// ensure a default calendar is available
		$stmt = $this->pdo->prepare('SELECT principaluri FROM calendars WHERE principaluri = ? LIMIT 1');
		$stmt->execute(array($principalUri));

		$result = $stmt->fetchAll();
		$hasDefaultCalendar = (count($result) != 0);
		
		if(!$hasPrinicpal)
		{
			// Adding the main principal
			$stmt = $this->pdo->prepare("INSERT IGNORE INTO principals (uri,email,displayname) VALUES (?, ?, ?)");
			$stmt->execute(array($principalUri, $username, ''));
		}
		
		if(!$hasDefaultCalendar)
		{
			$stmt = $this->pdo->prepare('INSERT IGNORE INTO calendars (principaluri, displayname, uri, description, components, ctag, calendarcolor) VALUES (?, ?, ?, "", "VEVENT,VTODO", 1, "#0002BFFF")');
			$stmt->execute(array($principalUri, 'Private', 'calendar'));
		}
		
		$this->provisioned[$username] = true;
		
		return true;
		
	}

}
