<?php
/**
 * EAuthServiceBase class file.
 *
 * @author Maxim Zemskov <nodge@yandex.ru>
 * @link http://code.google.com/p/yii-eauth/
 * @license http://www.opensource.org/licenses/bsd-license.php
 */

require_once 'IAuthService.php';

/**
 * EAuthServiceBase is a base class for providers. 
 * @package application.extensions.eauth
 */
abstract class EAuthServiceBase extends CComponent implements IAuthService {
	
	/**
	 * @var string the service name.
	 */
	protected $name;
	
	/**
	 *
	 * @var string the service title to display in views. 
	 */
	protected $title;
	
	/**
	 * @var string the service type (e.g. OpenID, OAuth).
	 */
	protected $type;
	
	/**
	 * @var array arguments for the jQuery.eauth() javascript function.
	 */
	protected $jsArguments = array();
	
	/**
	 * @var array authorization attributes.
	 * @see getAttribute
	 * @see getItem
	 */
	protected $attributes = array();
	
	/**
	 * @var boolean whether user was successfuly authenticated.
	 * @see getIsAuthenticated
	 */
	protected $authenticated = false;
	
	/**
	 * @var boolean whether is attributes was fetched.
	 */
	private $fetched = false;
	
	/**
	 * @var EAuth the {@link EAuth} application component.
	 */
	private $component;
	
	/**
	 * @var string the redirect url after successful authorization.
	 */
	private $redirectUrl = '';
	
	/**
	 * @var string the redirect url after unsuccessful authorization (e.g. user canceled).
	 */
	private $cancelUrl = '';

	/**
	 * PHP getter magic method.
	 * This method is overridden so that service attributes can be accessed like properties.
	 * @param string $name property name.
	 * @return mixed property value.
	 * @see getAttribute
	 */
	public function __get($name) {
		if ($this->hasAttribute($name))
			return $this->getAttribute($name);
		else
			return parent::__get($name);
	}

	/**
	 * Checks if a attribute value is null.
	 * This method overrides the parent implementation by checking
	 * if the attribute is null or not.
	 * @param string $name the attribute name.
	 * @return boolean whether the attribute value is null.
	 */
	public function __isset($name) {
		if ($this->hasAttribute($name))
			return true;
		else
			return parent::__isset($name);
	}

	/**
	 * Initialize the component. 
	 * Sets the default {@link redirectUrl} and {@link cancelUrl}.
	 * @param EAuth $component the component instance.
	 * @param array $options properties initialization.
	 */
	public function init($component, $options = array()) {
		if (isset($component))
			$this->setComponent($component);
	
		foreach ($options as $key => $val)
			$this->$key = $val;
		
		$this->setRedirectUrl(Yii::app()->user->returnUrl);
		$server = Yii::app()->request->getHostInfo();
		$path = Yii::app()->request->getPathInfo();
		$this->setCancelUrl($server.'/'.$path);
	}
	
	/**
	 * Returns service name(id).
	 * @return string the service name(id).
	 */
	public function getServiceName() {
		return $this->name;
	}
	
	/**
	 * Returns service title.
	 * @return string the service title.
	 */
	public function getServiceTitle() {
		return $this->title;
	}
	
	/**
	 * Returns service type (e.g. OpenID, OAuth).
	 * @return string the service type (e.g. OpenID, OAuth). 
	 */
	public function getServiceType() {
		return $this->type;
	}
		
	/**
	 * Returns arguments for the jQuery.eauth() javascript function.
	 * @return array the arguments for the jQuery.eauth() javascript function. 
	 */
	public function getJsArguments() {
		return $this->jsArguments;
	}
	
	/**
	 * Sets {@link EAuth} application component
	 * @param EAuth $component the application auth component.
	 */
	public function setComponent($component) {
		$this->component = $component;
	}
	
	/**
	 * Returns the {@link EAuth} application component.
	 * @return EAuth the {@link EAuth} application component.
	 */
	public function getComponent() {
		return $this->component;
	}
	
	/**
	 * Sets redirect url after successful authorization.
	 * @param string url to redirect.
	 */
	public function setRedirectUrl($url) {
		$this->redirectUrl = $url;
	}
	
	/**
	 * Returns the redirect url after successful authorization.
	 * @return string the redirect url after successful authorization.
	 */
	public function getRedirectUrl() {
		return $this->redirectUrl;
	}
	
	/**
	 * Sets redirect url after unsuccessful authorization (e.g. user canceled).
	 * @param string url to redirect.
	 */
	public function setCancelUrl($url) {
		$this->cancelUrl = $url;
	}
	
	/**
	 * Returns the redirect url after unsuccessful authorization (e.g. user canceled).
	 * @return string the redirect url after unsuccessful authorization (e.g. user canceled).
	 */
	public function getCancelUrl() {
		return $this->cancelUrl;
	}
	
	/**
	 * Authenticate the user.
	 * @return boolean whether user was successfuly authenticated.
	 */
	public function authenticate() {		
		return $this->getIsAuthenticated();
	}
	
	/**
	 * Whether user was successfuly authenticated.
	 * @return boolean whether user was successfuly authenticated.
	 */
	public function getIsAuthenticated() {
        return $this->authenticated;
    }
	
	/**
	 * Redirect to the url. If url is null, {@link redirectUrl} will be used.
	 * @param string $url url to redirect.
	 */
	public function redirect($url = null) {
		$this->component->redirect(isset($url) ? $url : $this->redirectUrl, true);
	}
	
	/**
	 * Redirect to the {@link cancelUrl} or simply close the popup window.
	 */
	public function cancel($url = null) {
		$this->component->redirect(isset($url) ? $url : $this->cancelUrl, false);
	}
	
	/**
	 * Makes the curl request to the url.
	 * @param string $url url to request.
	 * @param array $options HTTP request options. Keys: query, data, referer.
	 * @param boolean $parseJson Whether to parse response in json format.
	 * @return string the response.
	 */
	protected function makeRequest($url, $options = array(), $parseJson = true) {
		$ch = $this->initRequest($url, $options);
		
		$result = curl_exec($ch);
		$headers = curl_getinfo($ch);

		if (curl_errno($ch) > 0)
			throw new EAuthException(curl_error($ch), curl_errno($ch));
		
		/*echo 'result=<pre>';
		print_r($result);
		echo '</pre>';
		
		echo 'headers=<pre>';
		print_r($headers);
		echo '</pre>';
		exit;*/
		
		if ($headers['http_code'] != 200) {
			Yii::log(
				'Invalid response http code: '.$headers['http_code'].'.'.PHP_EOL.
				'URL: '.$url.PHP_EOL.
				'Options: '.var_export($options, true).PHP_EOL.
				'Result: '.$result,
				CLogger::LEVEL_ERROR, 'application.extensions.eauth'
			);
			throw new EAuthException(Yii::t('module_socialauth', 'Invalid response http code: {code}.', array('{code}' => $headers['http_code'])), $headers['http_code']);
		}
		
		curl_close($ch);
				
		if ($parseJson)
			$result = $this->parseJson($result);
		
		return $result;
	}
	
	/**
	 * Initializes a new session and return a cURL handle.
	 * @param string $url url to request.
	 * @param array $options HTTP request options. Keys: query, data, referer.
	 * @param boolean $parseJson Whether to parse response in json format.
	 * @return cURL handle.
	 */
	protected function initRequest($url, $options = array()) {
		$ch = curl_init();		
		//curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // error with open_basedir or safe mode
		//curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_setopt($ch, CURLOPT_HEADER, 0);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
		curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
		
		if (isset($options['referer']))
			curl_setopt($ch, CURLOPT_REFERER, $options['referer']);
		
		if (isset($options['headers']))
			curl_setopt($ch, CURLOPT_HTTPHEADER, $options['headers']);
		
		if (isset($options['query'])) {
			$url_parts = parse_url($url);
			if (isset($url_parts['query'])) {
				$query = $url_parts['query'];
				if (strlen($query) > 0)
					$query .= '&';
				$query .= http_build_query($options['query']);
				$url = str_replace($url_parts['query'], $query, $url);
			}
			else {
				$url_parts['query'] = $options['query'];
				$new_query = http_build_query($url_parts['query']);
				$url .= '?'.$new_query;
			}					
		}
		
		if (isset($options['data'])) {
			curl_setopt($ch, CURLOPT_POST, 1);
			curl_setopt($ch, CURLOPT_POSTFIELDS, $options['data']);
		}
		
		curl_setopt($ch, CURLOPT_URL, $url);
		return $ch;
	}
		
	/**
	 * Parse response from {@link makeRequest} in json format and check OAuth errors.
	 * @param string $response Json string.
	 * @return object result.
	 */
	protected function parseJson($response) {
		try {
			$result = json_decode($response);
			$error = $this->fetchJsonError($result);
			if (!isset($result)) {
				throw new EAuthException(Yii::t('module_socialauth', 'Invalid response format.', array()), 500);
			}
			else if (isset($error)) {
				throw new EAuthException($error['message'], $error['code']);
			}
			else
				return $result;
		}
		catch(Exception $e) {
			throw new EAuthException($e->getMessage(), $e->getCode());
		}
	}
	
	/**
	 * Returns the error info from json.
	 * @param stdClass $json the json response.
	 * @return array the error array with 2 keys: code and message. Should be null if no errors.
	 */
	protected function fetchJsonError($json) {
		if (isset($json->error)) {
			return array(
				'code' => 500,
				'message' => 'Unknown error occurred.',
			);
		}
		else
			return null;
	}
	
	/**
	 * @return string a prefix for the name of the session variables storing eauth session data.
	 */
	protected function getStateKeyPrefix() {
		return '__eauth_'.$this->getServiceName().'__';
	}
	
	/**
	 * Stores a variable in eauth session.
	 * @param string $key variable name.
	 * @param mixed $value variable value.
	 * @param mixed $defaultValue default value. If $value===$defaultValue, the variable will be
	 * removed from the session.
	 * @see getState
	 */
	protected function setState($key, $value, $defaultValue = null) {
		$session = Yii::app()->session;
		$key = $this->getStateKeyPrefix().$key;
		if($value === $defaultValue)
			unset($session[$key]);
		else
			$session[$key] = $value;
	}
	
	/**
	 * Returns a value indicating whether there is a state of the specified name.
	 * @param string $key state name.
	 * @return boolean whether there is a state of the specified name.
	 */
	protected function hasState($key) {
		$session = Yii::app()->session;
		$key = $this->getStateKeyPrefix().$key;
		return isset($session[$key]);
	}
	
	/**
	 * Returns the value of a variable that is stored in eauth session.
	 * @param string $key variable name.
	 * @param mixed $defaultValue default value.
	 * @return mixed the value of the variable. If it doesn't exist in the session,
	 * the provided default value will be returned.
	 * @see setState
	 */
	protected function getState($key, $defaultValue = null) {
		$session = Yii::app()->session;
		$key = $this->getStateKeyPrefix().$key;
		return isset($session[$key]) ? $session[$key] : $defaultValue;
	}
	
	/**
	 * Fetch attributes array.
	 * @return boolean whether the attributes was successfully fetched.
	 */
	protected function fetchAttributes() {
		return true;
	}
	
	/**
	 * Fetch attributes array.
	 * This function is internally used to handle fetched state.
	 */
	protected function _fetchAttributes() {
		if (!$this->fetched) {
			$this->fetched = true;
			$result = $this->fetchAttributes();
			if (isset($result))
				$this->fetched = $result;
		}
	}
	
	/**
	 * Returns the user unique id.
	 * @return mixed the user id.
	 */
	public function getId() {
		$this->_fetchAttributes();
		return $this->attributes['id'];
	}
	
	/**
	 * Returns the array that contains all available authorization attributes.
	 * @return array the attributes.
	 */
	public function getAttributes() {
		$this->_fetchAttributes();
		$attributes = array();
		foreach ($this->attributes as $key => $val) {
			$attributes[$key] = $this->getAttribute($key);
		}
		return $attributes;
	}
	
	/**
	 * Returns the authorization attribute value.
	 * @param string $key the attribute name.
	 * @param mixed $default the default value.
	 * @return mixed the attribute value.
	 */
	public function getAttribute($key, $default = null) {
		$this->_fetchAttributes();
		$getter = 'get'.$key;
		if (method_exists($this, $getter))
			return $this->$getter();
		else
			return isset($this->attributes[$key]) ? $this->attributes[$key] : $default;
	}
	
	/**
	 * Whether the authorization attribute exists.
	 * @param string $key the attribute name.
	 * @return boolean true if attribute exists, false otherwise.
	 */
	public function hasAttribute($key) {
		$this->_fetchAttributes();
		return isset($this->attributes[$key]);
	}
	
	/**
	 * Returns the object with a human-readable representation of the current authorization.
	 * @return stdClass the object.
	 */
	public function getItem() {
		$item = new stdClass;
		$item->title = $this->getAttribute('name');
		if (empty($this->title))
			$item->title = $this->getId();
		if ($this->hasAttribute('url'))
			$item->url = $this->getAttribute('url');
		return $item;
	}
		
	/**
	 * Returns the array that contains all available authorization attributes.
	 * @return array the attributes.
	 * @deprecated because getAttributes is more semantic.
	 */
	public function getItemAttributes() {
		return $this->getAttributes();
	}
}