1<?php 2 3require_once 'ClientException.php'; 4require_once 'ConnectionException.php'; 5require_once 'RequestException.php'; 6require_once 'ResponseException.php'; 7require_once 'Server.php'; 8require_once 'Namespace.php'; 9require_once 'Response.php'; 10 11abstract class XBMC_RPC_Client { 12 13 /** 14 * @var XBMC_RPC_Server A server object instance representing the server 15 * to be used for remote procedure calls. 16 * @access protected 17 */ 18 protected $server; 19 20 /** 21 * @var XBMC_RPC_Namespace The root namespace instance. 22 * @access private 23 */ 24 private $rootNamespace; 25 26 /** 27 * @var bool A flag to indicate if the JSON-RPC version is legacy, ie before 28 * the XBMC Eden updates. This can be used to determine the format of commands 29 * to be used with this library, allowing client code to support legacy systems. 30 * @access private 31 */ 32 private $isLegacy = false; 33 34 /** 35 * Constructor. 36 * 37 * Connects to the server and populates a list of available commands by 38 * having the server introspect. 39 * 40 * @param mixed $parameters An associative array of connection parameters, 41 * or a valid connection URI as a string. If supplying an array, the following 42 * paramters are accepted: host, port, user and pass. Any other parameters 43 * are discarded. 44 * @exception XBMC_RPC_ConnectionException if it is not possible to connect to 45 * the server. 46 * @access public 47 */ 48 public function __construct($parameters) { 49 try { 50 $server = new XBMC_RPC_Server($parameters); 51 } catch (XBMC_RPC_ServerException $e) { 52 throw new XBMC_RPC_ConnectionException($e->getMessage(), $e->getCode(), $e); 53 } 54 $this->server = $server; 55 $this->prepareConnection(); 56 $this->assertCanConnect(); 57 $this->createRootNamespace(); 58 } 59 60 /** 61 * Delegates any direct Command calls to the root namespace. 62 * 63 * @param string $name The name of the called command. 64 * @param mixed $arguments An array of arguments used to call the command. 65 * @return The result of the command call as returned from the namespace. 66 * @exception XBMC_RPC_InvalidCommandException if the called command does not 67 * exist in the root namespace. 68 * @access public 69 */ 70 public function __call($name, array $arguments) { 71 return call_user_func_array(array($this->rootNamespace, $name), $arguments); 72 } 73 74 /** 75 * Delegates namespace accesses to the root namespace. 76 * 77 * @param string $name The name of the requested namespace. 78 * @return XBMC_RPC_Namespace The requested namespace. 79 * @exception XBMC_RPC_InvalidNamespaceException if the namespace does not 80 * exist in the root namespace. 81 * @access public 82 */ 83 public function __get($name) { 84 return $this->rootNamespace->$name; 85 } 86 87 /** 88 * Executes a remote procedure call using the supplied XBMC_RPC_Command 89 * object. 90 * 91 * @param XBMC_RPC_Command The command to execute. 92 * @return XBMC_RPC_Response The response from the remote procedure call. 93 * @access public 94 */ 95 public function executeCommand(XBMC_RPC_Command $command) { 96 return $this->sendRpc($command->getFullName(), $command->getArguments()); 97 } 98 99 /** 100 * Determines if the XBMC system to which the client is connected is legacy 101 * (pre Eden) or not. This is useful because the format of commands/params 102 * is different in the Eden RPC implementation. 103 * 104 * @return bool True if the system is legacy, false if not. 105 * @access public 106 */ 107 public function isLegacy() { 108 return $this->isLegacy; 109 } 110 111 /** 112 * Asserts that the server is reachable and a connection can be made. 113 * 114 * @return void 115 * @exception XBMC_RPC_ConnectionException if it is not possible to connect to 116 * the server. 117 * @abstract 118 * @access protected 119 */ 120 protected abstract function assertCanConnect(); 121 122 /** 123 * Prepares for a connection to XBMC. 124 * 125 * Should be used by child classes for any pre-connection logic which is necessary. 126 * 127 * @return void 128 * @exception XBMC_RPC_ClientException if it was not possible to prepare for 129 * connection successfully. 130 * @abstract 131 * @access protected 132 */ 133 protected abstract function prepareConnection(); 134 135 /** 136 * Sends a JSON-RPC request to XBMC and returns the result. 137 * 138 * @param string $json A JSON-encoded string representing the remote procedure call. 139 * This string should conform to the JSON-RPC 2.0 specification. 140 * @param string $rpcId The unique ID of the remote procedure call. 141 * @return string The JSON-encoded response string from the server. 142 * @exception XBMC_RPC_RequestException if it was not possible to make the request. 143 * @access protected 144 * @link http://groups.google.com/group/json-rpc/web/json-rpc-2-0 JSON-RPC 2.0 specification 145 */ 146 protected abstract function sendRequest($json, $rpcId); 147 148 /** 149 * Build a JSON-RPC 2.0 compatable json_encoded string representing the 150 * specified command, parameters and request id. 151 * 152 * @param string $command The name of the command to be called. 153 * @param mixed $params An array of paramters to be passed to the command. 154 * @param string $rpcId A unique string used for identifying the request. 155 * @access private 156 */ 157 private function buildJson($command, $params, $rpcId) { 158 $data = array( 159 'jsonrpc' => '2.0', 160 'method' => $command, 161 'params' => $params, 162 'id' => $rpcId 163 ); 164 return json_encode($data); 165 } 166 167 /** 168 * Ensures that the recieved response from a remote procedure call is valid. 169 * 170 * $param XBMC_RPC_Response $response A response object encapsulating remote 171 * procedure call response data as returned from Client::sendRequest(). 172 * @return bool True of the reponse is valid, false if not. 173 * @access private 174 */ 175 private function checkResponse(XBMC_RPC_Response $response, $rpcId) { 176 return ($response->getId() == $rpcId); 177 } 178 179 /** 180 * Creates the root namespace instance. 181 * 182 * @return void 183 * @access private 184 */ 185 private function createRootNamespace() { 186 $commands = $this->loadAvailableCommands(); 187 $this->rootNamespace = new XBMC_RPC_Namespace('root', $commands, $this); 188 } 189 190 /** 191 * Generates a unique string to be used as a remote procedure call ID. 192 * 193 * @return string A unique string. 194 * @access private 195 */ 196 private function getRpcId() { 197 return uniqid(); 198 } 199 200 /** 201 * Retrieves an array of commands by requesting the RPC server to introspect. 202 * 203 * @return mixed An array of available commands which may be executed on the server. 204 * @exception XBMC_RPC_RequestException if it is not possible to retrieve a list of 205 * available commands. 206 * @access private 207 */ 208 private function loadAvailableCommands() { 209 try { 210 $response = $this->sendRpc('JSONRPC.Introspect'); 211 } catch (XBMC_RPC_Exception $e) { 212 throw new XBMC_RPC_RequestException( 213 'Unable to retrieve list of available commands: ' . $e->getMessage() 214 ); 215 } 216 if (isset($response['commands'])) { 217 $this->isLegacy = true; 218 return $this->loadAvailableCommandsLegacy($response); 219 } 220 $commands = array(); 221 foreach (array_keys($response['methods']) as $command) { 222 $array = $this->commandStringToArray($command); 223 $commands = $this->mergeCommandArrays($commands, $array); 224 } 225 return $commands; 226 } 227 228 /** 229 * Retrieves an array of commands by requesting the RPC server to introspect. 230 * 231 * This method supports the legacy implementation of XBMC's RPC. 232 * 233 * @return mixed An array of available commands which may be executed on the server. 234 * @access private 235 */ 236 private function loadAvailableCommandsLegacy($response) { 237 $commands = array(); 238 foreach ($response['commands'] as $command) { 239 $array = $this->commandStringToArray($command['command']); 240 $commands = $this->mergeCommandArrays($commands, $array); 241 } 242 return $commands; 243 } 244 245 /** 246 * Converts a dot-delimited command name to a multidimensional array format. 247 * 248 * @return mixed An array representing the command. 249 * @access private 250 */ 251 private function commandStringToArray($command) { 252 $path = explode('.', $command); 253 if (count($path) === 1) { 254 $commands[] = $path[0]; 255 return array(); 256 } 257 $command = array_pop($path); 258 $array = array(); 259 $reference =& $array; 260 foreach ($path as $i => $key) { 261 if (is_numeric($key) && intval($key) > 0 || $key === '0') { 262 $key = intval($key); 263 } 264 if ($i === count($path) - 1) { 265 $reference[$key] = array($command); 266 } else { 267 if (!isset($reference[$key])) { 268 $reference[$key] = array(); 269 } 270 $reference =& $reference[$key]; 271 } 272 } 273 return $array; 274 } 275 276 /** 277 * Recursively merges the supplied arrays whilst ensuring that commands are 278 * not duplicated within a namespace. 279 * 280 * Note that array_merge_recursive is not suitable here as it does not ensure 281 * that values are distinct within an array. 282 * 283 * @param mixed $base The base array into which $append will be merged. 284 * @param mixed $append The array to merge into $base. 285 * @return mixed The merged array of commands and namespaces. 286 * @access private 287 */ 288 private function mergeCommandArrays(array $base, array $append) { 289 foreach ($append as $key => $value) { 290 if (!array_key_exists($key, $base) && !is_numeric($key)) { 291 $base[$key] = $append[$key]; 292 continue; 293 } 294 if (is_array($value) || is_array($base[$key])) { 295 $base[$key] = $this->mergeCommandArrays($base[$key], $append[$key]); 296 } elseif (is_numeric($key)) { 297 if (!in_array($value, $base)) { 298 $base[] = $value; 299 } 300 } else { 301 $base[$key] = $value; 302 } 303 } 304 return $base; 305 } 306 307 /** 308 * Executes a remote procedure call using the supplied command name and parameters. 309 * 310 * @param string $command The full, dot-delimited name of the command to call. 311 * @param mixed $params An array of parameters to be passed to the called method. 312 * @return mixed The data returned from the response. 313 * @exception XBMC_RPC_RequestException if it was not possible to make the request. 314 * @exception XBMC_RPC_ResponseException if the response was not being properly received. 315 * @access private 316 */ 317 private function sendRpc($command, $params = array()) { 318 $rpcId = $this->getRpcId(); 319 $json = $this->buildJson($command, $params, $rpcId); 320 $response = new XBMC_RPC_Response($this->sendRequest($json, $rpcId)); 321 if (!$this->checkResponse($response, $rpcId)) { 322 throw new XBMC_RPC_ResponseException('JSON RPC request/response ID mismatch'); 323 } 324 return $response->getData(); 325 } 326 327} 328