<?php
	// FIXME: this doesn't work on Windows
	// FIXME: doesn't do server side ApiObjects nor nested ApiProviders right
	// FIXME: nested calls seem to have something wrong, either here
	// or on the D end...
	/**************************************
	*	This file is meant to help integrate web.d apps
	*	with PHP apps that live on the same domain, or remotely
	* 	if you've implemented OAuth authentication on the server.
	*
	* 	It's useful for things like single sign on with a web.d core.
	**************************************/

/// This provides (currently) *read-only* access to the web.d session file.
/// It needs access to the session ID cookie, so this has to live on the
/// same domain as your D app, and the D app's cookie should be set to
/// a path permissive enough to be sent here too.
///
/// Note this must also be on the same physical server as web.d (or at
/// least mount the tmp dirs to the same place), since it reads the session
/// data off the local filesystem.
///
/// TIP: you might want to use this to access your ApiProvider methods in D,
/// instead of reading the session alone.
class WebDotDSession {
	/// Access and load the session
	public function __construct($cookieName = "_sess_id") {
		if(isset($_COOKIE[$cookieName])) {
			$token = $_COOKIE[$cookieName];

			$this->sessionId = hash("sha256",
				$_SERVER["REMOTE_ADDR"] . "\r\n" .
				$_SERVER["HTTP_USER_AGENT"] . "\r\n" .
				$token);

			$path = "/tmp/arsd_session_file_" . $this->sessionId;
			if(file_exists($path)) {
				$filecontents = file_get_contents($path);
				$this->data = json_decode($filecontents);
				$this->fileHash = hash("sha256", $filecontents);
			}
		}
	}

	public $sessionId = "";
	public $fileHash = "";

	/// The data in the session, as a PHP object. Note that any writes
	/// to it will be discarded.
	public $data = null;
}

/// This provides a base for exceptions thrown by D
class WebDotDException extends Exception {}

// internal helper function; assoc arrays should be encoded differently than
// linear arrays
function arsd_helper_is_assoc_array($v) {
	if(!is_array($v)) return false;
	return array_diff_assoc(array_keys($v), range(0, count($v)));
}

/**
	If you've used the Javascript generated by web.d, you'll
	find this very familiar; this is a port of that for the
	most part.

	The ApiProvider methods don't make a call directly. Instead,
	they return an object that you can tweak a little, pass
	to other functions, and retreive the data.

	When getting synchronously, D exceptions are translated to PHP
	exceptions, and the return value is returned right here. Use
	the getSync() method to do this.

	NOT IMPLEMENTED IN PHP
	When getting asynchrously, D exceptions are sent to an onError
	delegate, and successes are sent to a handler delegate. Use
	the get() method to do this.
	DONE WITH NOT IMPLEMENTED

	For example:

	// using it anonymously (probably the easiest way)
	$result = $api->getMyData("hello")->getSync(); // it waits for the 
						// D to respond. Fast if
						// local, but can be slow if
						// accessing remote servers.

	// using it with a name
	$call = $api->getMyData("hello");
	// you can change the returned format with a method on the object
	$call->format("html");
	// and now get it
	$result = $call.getSync();

	// you can also chain method calls in one line
	$result = $api->getMyData("hello")->format("html")->getSync();


	You can also change the parameters of the request. You shouldn't
	need this most the time, but sometimes it's useful to have more
	control.

	$call = $api->getMyData("hello");
	// add an additional request param
	$call->setValue("my-request-param", "whatever");
	$call->setMethod("POST"); // override the default HTTP verb for the call

	$call->getSync(); // execute the request


	But, more often than not, you can use the call pretty easily with the
	anonymous one-liner.
*/
class WebDotDMethodCall {
	public function __construct($apiProvider, $httpMethod, $functionName, $url, $params) {
		$this->apiProvider = $apiProvider;
		$this->method = $httpMethod;
		$this->url = $url;
		$this->requestedDataFormat = "json";
		$this->functionName = $functionName;
		$num = 0;
		foreach($params as $arg) {
			$this->urlArgs["positional-arg-" . $num] = $arg;
			$num++;
		}
	}

	public function format($dataFormat) {
		$this->requestedDataFormat = $dataFormat;
		return $this;
	}

	public function setMethod($httpMethod) {
		$this->method = $httpMethod;
		return $this;
	}

	public function setValue($name, $value) {
		$this->urlArgs[$name] = $value;
		return $this;
	}

	private $apiProvider;
	private $url;
	private $urlArgs = array();
	public $method; /* public because php doesn't have friends... */
	private $requestedDataFormat;
	private $functionName;

	public function getSync() {

		$params = $this->urlArgs;
		$params["envelopeFormat"] = "json";
		$params["format"] = $this->requestedDataFormat;

		$this->apiProvider->addCustomRequestData($this, $params);

		$args = $this->getArgString($params);

		$url = $this->url;

		$postData = "";

		if($this->method == "GET")
			$url .= "?" . $args;
		else
			$postData = $args;

		$ch = curl_init($url);
		if($this->method == "POST") {
			curl_setopt($ch, CURLOPT_POST,1);
			curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
		}
		curl_setopt($ch, CURLOPT_HEADER,0);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);

		$this->apiProvider->addCustomCurlCode($ch, $this, $url, $postData);

		$data = curl_exec($ch);

		$resultObj = json_decode($data);

		if($resultObj == null) {
			throw new WebDotDException("Got null JSON. Instead, got: " . $data);
		}

		if($resultObj->success) {
			return $this->formatResult($resultObj->result);
		} else {
			// FIXME: maybe we can use type for better info?
			$msg = $resultObj->type;
			if(strlen($msg) > 0)
				$msg .= ": ";
			$msg .= $resultObj->errorMessage;
			throw new WebDotDException($msg);
		}

		// assert(0); // not reached
	}

	// Why do I bother with this? It converts any generic object returned
	// in the json to a custom class that, for now, only overrides toString,
	// to make accessing the secondary format easier, but might offer more
	// stuff in the future too, like better integration with the returned D types.
	protected function formatResult($arg) {
		if(is_object($arg)) {
			$vars = get_object_vars($arg);

			$obj = new WebDotDResultObject();

			foreach($vars as $k => $v)
				$obj->_fields[$k] = $this->formatResult($v);

			return $obj;
		} else if(is_array($arg)) {
			$arr = Array();
			foreach($arg as $k => $v)
				$arr[$k] = $this->formatResult($v);
			return $arr;
		} else
			return $arg;
	}

	private function argToUrlParam($k, $arg) {
		if(is_object($arg) && get_class($arg) == "WebDotDMethodCall") {
			$args = urlencode($k) . "=" . urlencode($arg->getRelativeUrlForNesting());
			$args .= "&".$k."-type=ServerResult";
			return $args;
		} else if(is_object($arg) || arsd_helper_is_assoc_array($arg)) {
			$aa = Array();
			if(get_class($arg) == "WebDotDResultObject")
				$aa = $arg->_fields;
			else if(is_object($arg))
				$aa = get_object_vars($arg);
			else
				$aa = $arg;

			// FIXME - needs to actually send it. web.d doesn't define how it
			// actually receives associative arrays yet.
			throw new Exception("not yet able to do object params");
		} else if(is_array($arg)) {
			$args = "";
			$outputtedHere = false;
			foreach($arg as $a) {
				if($outputtedHere) {
					$args .= "&";
				} else
					$outputtedHere = true;

				$args .= argToUrlParam($k, $arg);
			}

			return $args;
		} else {
			$args = "";
			$args .= urlencode($k);
			$args .= "=";
			$args .= urlencode($arg);
			return $args;
		}
	}

	private function getArgString($params = null) {
		if($params === null)
			$params = $this->urlArgs;
		$outputted = false;
		$args = "";
		foreach($params as $k => $arg) {
			if($outputted) {
				$args .= "&";
			} else {
				$outputted = true;
			}

			$args .= $this->argToUrlParam($k, $arg);
		}

		return $args;
	}

	private function getRelativeUrlForNesting() {
		return $this->functionName . "?" . $this->getArgString();
	}
}

/// A simple class to represent objects returned by calling D functions
class WebDotDResultObject {
	public $_fields = Array();

	public function __get($name) {
		return $this->_fields[$name];
	}

	public function __set($name, $value) {
		$this->_fields[$name] = $value;
	}

	public function __toString() {
		if(isset($this->_fields["formattedSecondarily"]))
			return $this->_fields["formattedSecondarily"];
		return json_encode($this->_fields);
	}
}

/// Base class for accessing web.d ApiProviders.
/// It doesn't do authentication, so you should use a subclass of it.
/// If you are on the same server as the api you are accessing, you can use
/// WebDotDLocalApiProvider, passing it a session object. This works with
/// all web.d classes, since the local authentication is built in.
///
/// Developers: you might use this as a base for your own remote classes,
/// if you are making a web service, adding some authentication or
/// custom branding. See the protected functions for available hooks.
class WebDotDApiProvider {
	protected $endPoint;
	/// The endpoint is a full URL to the base of your web.d program, with trailing slash.
	/// for example: http://mywebsite.com/myapp/
	public function __construct($endPoint) {
		$this->endPoint = $endPoint;
	}

	/// this can be used to add additional data to a request being prepared by mutating args
	public function addCustomRequestData($apiRequest, &$args) { }

	/// use this to manipulate the http request a little before it is sent (custom headers, etc.)
	public function addCustomCurlCode($ch, $apiRequest, $url, $postData) { }

	/// Returns a lazy method call object. The arguments and name are dynamic, so
	/// you can do $api->anyFunction($any, $args)->getSync();
	/// The getSync() command should be run at the last possible moment, when you
	/// need to convert the call into a PHP variable result.
	///
	/// Note: you can pass those objects directly as arguments to other functions
	/// to combine calls into one request! (Much of the time. It's not perfect server side,
	/// but it works on primitives at least. This will eventually change.)
	///
	/// See WebDotDMethodCall's documentation for more information.
	public function __call($name, $params = null) {
		$url = $this->endPoint . $name;

		return new WebDotDMethodCall($this,
			// the naming convention currently used by Javascript in web.d too
			// is if a function name starts with get, use GET, otherwise, use POST.
			// This is imperfect, but it's not awful (to me anyway).
			// If you get the method wrong, the request will probably still work.
			strpos($name, "get") === 0 ? "GET" : "POST",
			$name,
			$url,
			$params);
	}
}

/// Provides access to a *local* D ApiProvider, authenticating via a session.
/// This just works on most web.d code.
class LocalWebDotDApiProvider extends WebDotDApiProvider {
	/// Takes a WebDotDSession
	/// The endpoint can be a relative path here if you like as long
	/// as it doesn't start with http. It will inherit the protocol and
	/// domain of the current request.
	/// FIXME: it should be a fully relative link.
	public function __construct($endPoint, $session) {
		if(strpos($endPoint, "http") !== 0) {
			$current = "http";
			if(isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"])
				$current .= "s";
			$current .= "://";
			$current .= $_SERVER["HTTP_HOST"];

			if(strpos($endPoint, "/") === 0)
				$current .= $endPoint;
			else
				die("Relative linking isn't really implemented well.");
			$endPoint = $current;
		}

		parent::__construct($endPoint);
		$this->session = $session;
	}

	protected $session;

	public function addCustomRequestData($apiRequest, &$args) {
		// we have to add the CSRF token or web.d will likely reject our command
		if($apiRequest->method != "POST")
			return; // no need for csrf token
		if(!isset($session->data["csrfToken"]))
			return; // we don't have one
		$decoded = array();
		$csrfData = explode("&", $session->data["csrfToken"]);
		foreach($csrfToken as $item) {
			$info = split("=", $item);
			$decoded[urldecode($info[0])] = urldecode($info[1]);
		}

		// add the token to the arguments
		$args[$decoded["key"]] = $decoded["token"];
	}

	public function addCustomCurlCode($ch, $apiRequest, $url, $postData) {
		// we want to ask D to also use the same session we're looking at
		// The full session ID tells it what to use, and the file hash proves
		// to D that we already have access to it.
		$magic = $this->session->sessionId . ";" . $this->session->fileHash;
		$headers = array("X-Arsd-Local: yes");
		if(strlen($magic) > 0)
			$headers[] = "X-Arsd-Session-Override: $magic";
		curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
	}
}

/// Provides access to a remote D ApiProvider, using OAuth authentication.
///
/// You'll almost certainly want to provide a subclass of this for actual use.
///
/// You'll also have to implement OAuth in your D code too (I'll post my library
/// to help with that to github once I clean it up for public use, and then
/// I'll see how much I can reasonably automate in web.d.)
class RemoteWebDotDApiProvider extends WebDotDApiProvider {
	/// The first three of these are specific to the system you are accessing.
	/// The apiKey and apiSecret are how you identify as a specific user.
	public function __construct($endPoint, $accessToken, $tokenSecret, $apiKey, $apiSecret) {
		parent::__construct($endPoint);

		$this->accessToken = $accessToken;
		$this->tokenSecret = $tokenSecret;

		$this->apiKey = $apiKey;
		$this->apiSecret = $apiSecret;
	}

	private $accessToken;
	private $tokenSecret;

	private $apiKey;
	private $apiSecret;

	public function addCustomCurlCode($ch, $apiRequest, $url, $postData) {
		$oauth = new ARSDOAuth;
		$oauthHeader = $oauth->getRequestHeader(
			$this->apiKey, $this->apiSecret,
			$url,
			$this->accessToken, $this->tokenSecret,
			$postData);

		curl_setopt($ch, CURLOPT_HTTPHEADER, array($oauthHeader));
	}
}

/* My implementation of OAuth 1 client requests, used for the remote authentication. A port from D. */

// Does custom sorting..
class ARSDPair {
	public $name;
	public $value;

	public function __construct($k, $v) {
		$this->name = $k;
		$this->value = $v;
	}

	public function __toString() {
		return urlencode($this->name) ."=" . urlencode($this->value);
	}

	static function opCmp($lhs, $rhs) {
		$val = strcmp($lhs->name, $rhs->name);

		if($val == 0)
			$val = strcmp($lhs->value, $rhs->value);

		return $val;
	}
}

// Actually does the signing
class ARSDOAuth {
	public function getRequestHeader($apiKey, $apiSecret, $url, $oauthToken, $oauthTokenSecret, $data) {
		$oauthValues = Array();
		$oauthValues["oauth_token"]            = $oauthToken;
		$oauthValues["token_secret"]           = $oauthTokenSecret;
		$oauthValues["oauth_consumer_key"]     = $apiKey;
		$oauthValues["oauth_nonce"]            = rand() . time();
		$oauthValues["oauth_signature_method"] = "HMAC-SHA1";
		$oauthValues["oauth_timestamp"]        = time();
		$oauthValues["oauth_version"]          = "1.0";

		$signWith = urlencode($apiSecret) . "&" . $oauthTokenSecret;

		$protocolHostAndPath = "";
		$queryStringContents = "";
		
		$questionMark = strpos($url, "?");
		if($questionMark === FALSE)
			$protocolHostAndPath = $url;
		else {
			$protocolHostAndPath = substr($url, 0, $questionMark);
			$queryStringContents = substr($url, $questionMark + 1, strlen($url));
		}

		$sig = $this->getSignature(
			strlen($data) > 0 ? "POST" : "GET",
			$protocolHostAndPath,
			$queryStringContents,
			$oauthValues,
			$data,
			$signWith);

		$oauthValues["oauth_signature"] = $sig;


		$oauthHeader = "";
		$outputted = false;
		foreach($oauthValues as $k => $v) {
			if($outputted)
				$oauthHeader .= ",";
			else
				$outputted = true;

			$oauthHeader .= $k . "=" . "\"" . $v . "\"";
		}

		return "Authorization: OAuth " . $oauthHeader;
	}

	// A port from D
	public function getSignature (
		$method,
		$protocolHostAndPath,
		$queryStringContents,
		$authorizationHeaderContents,
		$postBodyIfWwwEncoded,
		$signWith)
	{
			
		$baseString = "";

		$baseString .= $method;
		$baseString .= "&";
		$baseString .= urlencode($protocolHostAndPath);

		$baseString .= "&";

		$getArray = $this->decodeVariables($queryStringContents);
		$postArray = $this->decodeVariables($postBodyIfWwwEncoded);

		$pairs = Array(); // should hold Pairs

		foreach($getArray as $k => $vals)
			foreach($vals as $v)
				$pairs[]= new ARSDPair($k, $v);
		foreach($postArray as $k => $vals)
			foreach($vals as $v)
				$pairs[] = new ARSDPair($k, $v);
		foreach($authorizationHeaderContents as $k => $v)
			$pairs[] = new ARSDPair($k, $v);

		$outputted = false;

		$params = "";

		usort($pairs, "ARSDPair::opCmp");
		foreach($pairs as $pair) {
			if($outputted)
				$params.= "&";
			else
				$outputted = true;

			$params .= $pair;
		}

		$baseString .= urlencode($params);

		return urlencode(base64_encode(hash_hmac('sha1', $baseString, $signWith, TRUE)));
	}

	private function decodeVariables($str) {
		if(strlen($str) == 0)
			return array();
		$ret = array();
		$parts = explode("&", $str);

		foreach($parts as $part) {
			$kv = explode("=", $part);
			$k = urldecode($kv[0]);
			$v = "";
			if(count($kv) > 1)
				$v = urldecode($kv[1]);

			if(!isset($ret[$k]))
				$ret[$k] = array();
			$ret[$k][] = $v;
		}

		return $ret;
	}
}

