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; } }