<?php

/*

	Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.

	This file is part of the Fat-Free Framework (http://fatfreeframework.com).

	This is free software: you can redistribute it and/or modify it under the
	terms of the GNU General Public License as published by the Free Software
	Foundation, either version 3 of the License, or later.

	Fat-Free Framework is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
	General Public License for more details.

	You should have received a copy of the GNU General Public License along
	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.

*/

//! Authorization/authentication plug-in
class Auth {

	//@{ Error messages
	const
		E_LDAP='LDAP connection failure',
		E_SMTP='SMTP connection failure';
	//@}

	protected
		//! Auth storage
		$storage,
		//! Mapper object
		$mapper,
		//! Storage options
		$args,
		//! Custom compare function
		$func;

	/**
	*	Jig storage handler
	*	@return bool
	*	@param $id string
	*	@param $pw string
	*	@param $realm string
	**/
	protected function _jig($id,$pw,$realm) {
		$success = (bool)
			call_user_func_array(
				[$this->mapper,'load'],
				[
					array_merge(
						[
							'@'.$this->args['id'].'==?'.
							($this->func?'':' AND @'.$this->args['pw'].'==?').
							(isset($this->args['realm'])?
								(' AND @'.$this->args['realm'].'==?'):''),
							$id
						],
						($this->func?[]:[$pw]),
						(isset($this->args['realm'])?[$realm]:[])
					)
				]
			);
		if ($success && $this->func)
			$success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw']));
		return $success;
	}

	/**
	*	MongoDB storage handler
	*	@return bool
	*	@param $id string
	*	@param $pw string
	*	@param $realm string
	**/
	protected function _mongo($id,$pw,$realm) {
		$success = (bool)
			$this->mapper->load(
				[$this->args['id']=>$id]+
				($this->func?[]:[$this->args['pw']=>$pw])+
				(isset($this->args['realm'])?
					[$this->args['realm']=>$realm]:[])
			);
		if ($success && $this->func)
			$success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw']));
		return $success;
	}

	/**
	*	SQL storage handler
	*	@return bool
	*	@param $id string
	*	@param $pw string
	*	@param $realm string
	**/
	protected function _sql($id,$pw,$realm) {
		$success = (bool)
			call_user_func_array(
				[$this->mapper,'load'],
				[
					array_merge(
						[
							$this->args['id'].'=?'.
							($this->func?'':' AND '.$this->args['pw'].'=?').
							(isset($this->args['realm'])?
								(' AND '.$this->args['realm'].'=?'):''),
							$id
						],
						($this->func?[]:[$pw]),
						(isset($this->args['realm'])?[$realm]:[])
					)
				]
			);
		if ($success && $this->func)
			$success = call_user_func($this->func,$pw,$this->mapper->get($this->args['pw']));
		return $success;
	}

	/**
	*	LDAP storage handler
	*	@return bool
	*	@param $id string
	*	@param $pw string
	**/
	protected function _ldap($id,$pw) {
		$port=(int)($this->args['port']?:389);
		$filter=$this->args['filter']=$this->args['filter']?:"uid=".$id;
		$this->args['attr']=$this->args['attr']?:["uid"];
		array_walk($this->args['attr'],
		function($attr)use(&$filter,$id) {
			$filter=str_ireplace($attr."=*",$attr."=".$id,$filter);});
		$dc=@ldap_connect($this->args['dc'],$port);
		if ($dc &&
			ldap_set_option($dc,LDAP_OPT_PROTOCOL_VERSION,3) &&
			ldap_set_option($dc,LDAP_OPT_REFERRALS,0) &&
			ldap_bind($dc,$this->args['rdn'],$this->args['pw']) &&
			($result=ldap_search($dc,$this->args['base_dn'],
				$filter,$this->args['attr'])) &&
			ldap_count_entries($dc,$result) &&
			($info=ldap_get_entries($dc,$result)) &&
			$info['count']==1 &&
			@ldap_bind($dc,$info[0]['dn'],$pw) &&
			@ldap_close($dc)) {
			return in_array($id,(array_map(function($value){return $value[0];},
				array_intersect_key($info[0],
					array_flip($this->args['attr'])))),TRUE);
		}
        throw new \Exception(self::E_LDAP);
	}

	/**
	*	SMTP storage handler
	*	@return bool
	*	@param $id string
	*	@param $pw string
	**/
	protected function _smtp($id,$pw) {
		$socket=@fsockopen(
			(strtolower($this->args['scheme'])=='ssl'?
				'ssl://':'').$this->args['host'],
				$this->args['port']);
		$dialog=function($cmd=NULL) use($socket) {
			if (!is_null($cmd))
				fputs($socket,$cmd."\r\n");
			$reply='';
			while (!feof($socket) &&
				($info=stream_get_meta_data($socket)) &&
				!$info['timed_out'] && $str=fgets($socket,4096)) {
				$reply.=$str;
				if (preg_match('/(?:^|\n)\d{3} .+\r\n/s',
					$reply))
					break;
			}
			return $reply;
		};
		if ($socket) {
			stream_set_blocking($socket,TRUE);
			$dialog();
			$fw=Base::instance();
			$dialog('EHLO '.$fw->HOST);
			if (strtolower($this->args['scheme'])=='tls') {
				$dialog('STARTTLS');
				stream_socket_enable_crypto(
					$socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT);
				$dialog('EHLO '.$fw->HOST);
			}
			// Authenticate
			$dialog('AUTH LOGIN');
			$dialog(base64_encode($id));
			$reply=$dialog(base64_encode($pw));
			$dialog('QUIT');
			fclose($socket);
			return (bool)preg_match('/^235 /',$reply);
		}
        throw new \Exception(self::E_SMTP);
	}

	/**
	*	Login auth mechanism
	*	@return bool
	*	@param $id string
	*	@param $pw string
	*	@param $realm string
	**/
	function login($id,$pw,$realm=NULL) {
		return $this->{'_'.$this->storage}($id,$pw,$realm);
	}

	/**
	*	HTTP basic auth mechanism
	*	@return bool
	*	@param $func callback
	**/
	function basic($func=NULL) {
		$fw=Base::instance();
		$realm=$fw->REALM;
		$hdr=NULL;
		if (isset($_SERVER['HTTP_AUTHORIZATION']))
			$hdr=$_SERVER['HTTP_AUTHORIZATION'];
		elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']))
			$hdr=$_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
		if (!empty($hdr))
			list($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW'])=
				explode(':',base64_decode(substr($hdr,6)));
		if (isset($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']) &&
			$this->login(
				$_SERVER['PHP_AUTH_USER'],
				$func?
					$fw->call($func,$_SERVER['PHP_AUTH_PW']):
					$_SERVER['PHP_AUTH_PW'],
				$realm
			))
			return TRUE;
		if (PHP_SAPI!='cli')
			header('WWW-Authenticate: Basic realm="'.$realm.'"');
		$fw->status(401);
		return FALSE;
	}

	/**
	*	Instantiate class
	*	@return object
	*	@param $storage string|object
	*	@param $args array
	*	@param $func callback
	**/
	function __construct($storage,?array $args=NULL,$func=NULL) {
		if (is_object($storage) && is_a($storage,'DB\Cursor')) {
			$this->storage=$storage->dbtype();
			$this->mapper=$storage;
			unset($ref);
		}
		else
			$this->storage=$storage;
		$this->args=$args;
		$this->func=$func;
	}

}
