<?php

/**
* Abstract OAuth consumer
* @author Ben Tadiar <ben@handcraftedbyben.co.uk>
* @link https://github.com/benthedesigner/dropbox
* @package Dropbox\OAuth
* @subpackage Consumer
*/

abstract class IWP_Dropbox_OAuth_Consumer_ConsumerAbstract
{
	// Dropbox web endpoint
	const WEB_URL = 'https://www.dropbox.com/';

	// OAuth flow methods
	const REQUEST_TOKEN_METHOD = 'oauth2/REQUEST_TOKEN_METHOD';
	const AUTHORISE_METHOD = 'oauth2/authorize';
	const ACCESS_TOKEN_METHOD = 'oauth2/token';
	const API_URL = 'https://api.dropbox.com/1/';
	const OAUTH_UPGRADE = 'oauth2/token_from_oauth1';

	/**
	 * Signature method, either PLAINTEXT or HMAC-SHA1
	 * @var string
	 */
	private $sigMethod = 'PLAINTEXT';

	/**
	 * Output file handle
	 * @var null|resource
	 */
	protected $outFile = null;

	/**
	 * Input file handle
	 * @var null|resource
	 */
	protected $inFile = null;

	/**
	 * OAuth token
	 * @var stdclass
	 */
	private $token = null;

	/**
	* Acquire an unauthorised request token
	* @link http://tools.ietf.org/html/rfc5849#section-2.1
	* @return void
	*/

	protected function authenticate()
	{
	    global $iwp_backup_core;
	    
	    $access_token = $this->storage->get('access_token');
	    //Check if the new token type is set if not they need to be upgraded to OAuth2
	    if (!empty($access_token) && isset($access_token->oauth_token) && !isset($access_token->token_type)) {
	        $iwp_backup_core->log('OAuth v1 token found: upgrading to v2');
	        $this->upgradeOAuth();
	        $iwp_backup_core->log('OAuth token upgrade successful');
	    }
	    
	    if (empty($access_token) || !isset($access_token->oauth_token)) {
	        try {
	            $this->getAccessToken();
	        } catch(Exception $e) {
	            $excep_class = get_class($e);
	            // 04-Sep-2015 - Dropbox started throwing a 400, which caused a Dropbox_BadRequestException which previously wasn't being caught
	            if ('Dropbox_BadRequestException' == $excep_class || 'Dropbox_Exception' == $excep_class) {
	                global $iwp_backup_core;
	                $iwp_backup_core->log($e->getMessage().' - need to reauthenticate this site with Dropbox (if this fails, then you can also try wiping your settings from the Expert Settings section)');
	                //$this->getRequestToken();
	                $this->authorise();
	            } else {
	                throw $e;
	            }
	        }
	    }
	}
	
	public function getRequestToken()
	{
		$url = WPTC_Dropbox_API::API_URL_V2 . self::REQUEST_TOKEN_METHOD;
		$response = $this->fetch('POST', $url, '');

		return $this->parseTokenString($response['body']);
	}

	/**
	* Build the user authorisation URL
	* @return string
	*/
	public function getAuthoriseUrl()
	{
		$params = array(
			'client_id' => WPTC_DROPBOX_CLIENT_ID,
			'response_type' => 'code',
			'redirect_uri' => WPTC_DROPBOX_REDIRECT_URL,
			'state' => WPTC_DROPBOX_WP_REDIRECT_URL,
		);


		// Build the URL and redirect the user
		$query = '?' . http_build_query($params, '', '&');
		$url = self::WEB_URL . self::AUTHORISE_METHOD . $query;

		return $url;
	}

	public function upgradeOAuth()
	{
		// N.B. This call only exists under API v1 - i.e. there is no APIv2 equivalent. Hence the APIv1 endpoint (API_URL) is used, and not the v2 (API_URL_V2)

		$url = self::API_URL . self::OAUTH_UPGRADE;
		$config = WPTC_Factory::get('config');
		$this->token = new stdClass();
		$this->token->oauth_token = $config->get_option('access_token');
		$this->token->oauth_token_secret  = $config->get_option('access_token_secret');
		$response = $this->fetch('POST', $url, '');
		return $response['body'];
	}

	/**
	 * Acquire an access token
	 * Tokens acquired at this point should be stored to
	 * prevent having to request new tokens for each API call
	 * @link http://tools.ietf.org/html/rfc5849#section-2.3
	 */
	public function getAccessToken()
	{
		// Get the signed request URL
		$response = $this->fetch('POST', WPTC_Dropbox_API::API_URL_V2, self::ACCESS_TOKEN_METHOD);
		return $this->parseTokenString($response['body']);
	}

	/**
	 * Generate signed request URL
	 * See inline comments for description
	 * @link http://tools.ietf.org/html/rfc5849#section-3.4
	 * @param  string $method     HTTP request method
	 * @param  string $url        API endpoint to send the request to
	 * @param  string $call       API call to send
	 * @param  array  $additional Additional parameters as an associative array
	 * @return array
	 */
   protected function getSignedRequest($method, $url, $call, array $additional = array())
	{
		// Get the request/access token
		$token = $this->getToken();
		// Prepare the standard request parameters differently for OAuth1 and OAuth2; we still need OAuth1 to make the request to the upgrade token endpoint
		if (!empty($token)) {
			$params = array(
				'access_token' => $token,
			);

			/*
				To keep this API backwards compatible with the API v1 endpoints all v2 endpoints will also send to this method a api_v2 parameter this will then return just the access token as the signed request is not needed for any calls.
			 */

			if (isset($additional['api_v2']) && $additional['api_v2'] == true) {
				unset($additional['api_v2']);
				if (isset($additional['content_download']) && $additional['content_download'] == true) {
					unset($additional['content_download']);
					$headers = array(
						'Authorization: Bearer '.$params['access_token'],
						'Content-Type:',
						'Dropbox-API-Arg: '.json_encode($additional),
					);
					$additional = '';
				} else if (isset($additional['content_upload']) && $additional['content_upload'] == true) {
					unset($additional['content_upload']);
					$headers = array(
						'Authorization: Bearer '.$params['access_token'],
						'Content-Type: application/octet-stream',
						'Dropbox-API-Arg: '.json_encode($additional),
					);
					$additional = '';
				} else {
					$headers = array(
						'Authorization: Bearer '.$params['access_token'],
						'Content-Type: application/json',
					);
				}
				return array(
					'url' => $url . $call,
					'postfields' => $additional,
					'headers' => $headers,
				);
			}
		} else {
			
			// Generate a random string for the request
			$nonce = md5(microtime(true) . uniqid('', true));
			$params = array(
				'oauth_consumer_key' => WPTC_DROPBOX_CLIENT_ID,
				'oauth_token' => $this->token->oauth_token,
				'oauth_signature_method' => $this->sigMethod,
				'oauth_version' => '1.0',
				// Generate nonce and timestamp if signature method is HMAC-SHA1
				'oauth_timestamp' => ($this->sigMethod == 'HMAC-SHA1') ? time() : null,
				'oauth_nonce' => ($this->sigMethod == 'HMAC-SHA1') ? $nonce : null,
			);
		}

		// Merge with the additional request parameters
		$params = array_merge($params, $additional);
		ksort($params);

		// URL encode each parameter to RFC3986 for use in the base string
		$encoded = array();
		foreach($params as $param => $value) {
			if ($value !== null) {
				// If the value is a file upload (prefixed with @), replace it with
				// the destination filename, the file path will be sent in POSTFIELDS
				if (isset($value[0]) && $value[0] === '@') $value = $params['filename'];
				# Prevent spurious PHP warning by only doing non-arrays
				if (!is_array($value)) $encoded[] = $this->encode($param) . '=' . $this->encode($value);
			} else {
				unset($params[$param]);
			}
		}

		// Build the first part of the string
		$base = $method . '&' . $this->encode($url . $call) . '&';

		// Re-encode the encoded parameter string and append to $base
		$base .= $this->encode(implode('&', $encoded));

		// Concatenate the secrets with an ampersand
		$key = WPTC_DROPBOX_CLIENT_SECRET . '&' . $this->token->oauth_token_secret;

		// Get the signature string based on signature method
		$signature = $this->getSignature($base, $key);
		$params['oauth_signature'] = $signature;

		// Build the signed request URL
		$query = '?' . http_build_query($params, '', '&');
		return array(
			'url' => $url . $call . $query,
			'postfields' => $params,
		);
	}

	/**
	 * Generate the oauth_signature for a request
	 * @param string $base Signature base string, used by HMAC-SHA1
	 * @param string $key  Concatenated consumer and token secrets
	 */
	private function getSignature($base, $key)
	{
		switch ($this->sigMethod) {
			case 'PLAINTEXT':
				$signature = $key;
				break;
			case 'HMAC-SHA1':
				$signature = base64_encode(hash_hmac('sha1', $base, $key, true));
				break;
		}

		return $signature;
	}

	/**
	 * Set the token to use for OAuth requests
	 * @param stdtclass $token A key secret pair
	 */
	public function setToken($token)
	{

		$this->token = $token;

		return $this;
	}

	public function getToken()
	{
		return $this->token;
	}

	public function resetToken()
	{
		$token = new stdClass;
		$token->oauth_token = false;
		$token->oauth_token_secret = false;

		$this->setToken($token);

		return $this;
	}

	/**
	 * Set the OAuth signature method
	 * @param  string $method Either PLAINTEXT or HMAC-SHA1
	 * @return void
	 */
	public function setSignatureMethod($method)
	{
		$method = strtoupper($method);

		switch ($method) {
			case 'PLAINTEXT':
			case 'HMAC-SHA1':
				$this->sigMethod = $method;
				break;
			default:
				throw new Exception('Unsupported signature method ' . $method);
		}
	}

	/**
	 * Set the output file
	 * @param resource Resource to stream response data to
	 * @return void
	 */
	public function setOutFile($handle)
	{
		if (!is_resource($handle) || get_resource_type($handle) != 'stream') {
			throw new Exception('Outfile must be a stream resource');
		}
		$this->outFile = $handle;
	}

	/**
	 * Set the input file
	 * @param resource Resource to read data from
	 * @return void
	 */
	public function setInFile($handle)
	{
		$this->inFile = $handle;
	}

	/**
	* Parse response parameters for a token into an object
	* Dropbox returns tokens in the response parameters, and
	* not a JSON encoded object as per other API requests
	* @link http://oauth.net/core/1.0/#response_parameters
	* @param string $response
	* @return object stdClass
	*/
	private function parseTokenString($response)
	{
		if (!$response)
			throw new Exception('Response cannot be null');

		$parts = explode('&', $response);
		$token = new stdClass();
		foreach ($parts as $part) {
			list($k, $v) = explode('=', $part, 2);
			$k = strtolower($k);
			$token->$k = $v;
		}

		return $token;
	}

	/**
	 * Encode a value to RFC3986
	 * This is a convenience method to decode ~ symbols encoded
	 * by rawurldecode. This will encode all characters except
	 * the unreserved set, ALPHA, DIGIT, '-', '.', '_', '~'
	 * @link http://tools.ietf.org/html/rfc5849#section-3.6
	 * @param mixed $value
	 */
	private function encode($value)
	{
		return str_replace('%7E', '~', rawurlencode($value));
	}
}
