PageRenderTime 26ms CodeModel.GetById 0ms RepoModel.GetById 0ms app.codeStats 0ms

/classes/kohana/realplexor.php

https://github.com/vinks/kohana-realplexor
PHP | 358 lines | 185 code | 66 blank | 107 comment | 34 complexity | 2d3e7fb06368a3fa27c3e178d2f142be MD5 | raw file
  1. <?php defined('SYSPATH') OR die('No direct access allowed.');
  2. /**
  3. * Dklab_Realplexor PHP API for Kohana.
  4. *
  5. * @version 1.31
  6. */
  7. class Kohana_Realplexor {
  8. protected static $_instance;
  9. /**
  10. * Singleton pattern
  11. *
  12. * @return Realplexor
  13. */
  14. public static function instance($group = 'default')
  15. {
  16. if ( ! isset(Realplexor::$_instance))
  17. {
  18. // Load the configuration for this type
  19. $config = Kohana::$config->load('realplexor')->get($group);
  20. Fire_Helper::log($config, 'Config');
  21. // Create a new realplexor instance
  22. Realplexor::$_instance = new Realplexor($config);
  23. }
  24. return Realplexor::$_instance;
  25. }
  26. /**
  27. * Loads configuration options.
  28. *
  29. * @param array $config
  30. * @return void
  31. */
  32. public function __construct($config = array())
  33. {
  34. // Save the config in the object
  35. $this->_config = $config;
  36. // Set namespace
  37. $this->_namespace = $config['namespace'];
  38. if ($config['login'])
  39. $this->logon($config['login']);
  40. }
  41. /**
  42. * Set login and password to access Realplexor (if the server needs it).
  43. * This method does not check credentials correctness.
  44. *
  45. * @param string $login
  46. * @return void
  47. */
  48. public function logon($login)
  49. {
  50. // All keys must always be login-prefixed!
  51. $this->_namespace = $login . '_' . $this->_namespace;
  52. }
  53. /**
  54. * Send data to realplexor.
  55. *
  56. * Example usage:
  57. * ~~~ Send data to one channel
  58. * Realplexor::instance()->send("Alpha", array("here" => "is", "any" => array("structured", "data")));
  59. * ~~~ Send data to multiple channels at once.
  60. * Realplexor::instance()->send(array("Alpha", "Beta"), "any data");
  61. * ~~~ Send data limiting receivers.
  62. * Realplexor::instance()->send("Alpha", "any data", array($id1, $id2, ...));
  63. *
  64. * @param mixed $ids Target IDs in form of: array(id1 => cursor1, id2 => cursor2, ...)
  65. * of array(id1, id2, id3, ...). If sending to a single ID,
  66. * you may pass it as a plain string, not array.
  67. * @param mixed $data Data to be sent (any format, e.g. nested arrays are OK).
  68. * @param array $showonly Send this message to only those who also listen any of these IDs.
  69. * This parameter may be used to limit the visibility to a closed
  70. * number of cliens: give each client an unique ID and enumerate
  71. * client IDs in $showonly to not to send messages to others.
  72. * @return void
  73. */
  74. public function send($ids, $data, $showonly = null)
  75. {
  76. $data = json_encode($data);
  77. $pairs = array();
  78. foreach ((array)$ids as $id => $cursor)
  79. {
  80. if (is_int($id)) {
  81. $id = $cursor; // this is NOT cursor, but ID!
  82. $cursor = null;
  83. }
  84. if (!preg_match('/^\w+$/', $id))
  85. throw new Exception('Identifier must be alphanumeric, "' . $id . '" given');
  86. $id = $this->_namespace . $id;
  87. if ($cursor !== null)
  88. {
  89. if (!is_numeric($cursor))
  90. throw new Exception('Cursor must be numeric, "' . $cursor . '" given');
  91. $pairs[] = $cursor . ':' . $id;
  92. }
  93. else
  94. {
  95. $pairs[] = $id;
  96. }
  97. }
  98. if (is_array($showonly))
  99. {
  100. foreach ($showonly as $id) {
  101. $pairs[] = '*' . $this->_namespace . $id;
  102. }
  103. }
  104. $this->_send(join(',', $pairs), $data);
  105. }
  106. /**
  107. * Return list of online IDs (keys) and number of online browsers
  108. * for each ID. (Now "online" means "connected just now", it is
  109. * very approximate; more precision is in TODO.)
  110. *
  111. * @param array $prefixes If set, only online IDs with these prefixes are returned.
  112. * @return array List of matched online IDs (keys) and online counters (values).
  113. */
  114. public function onlinecounters($prefixes = null)
  115. {
  116. // Add namespace.
  117. $prefixes = $prefixes !== null ? (array) $prefixes : array();
  118. if (strlen($this->_namespace))
  119. {
  120. if (!$prefixes)
  121. $prefixes = array(''); // if no prefix passed, we still need namespace prefix
  122. foreach ($prefixes as $i => $idp)
  123. $prefixes[$i] = $this->_namespace . $idp;
  124. }
  125. // Send command.
  126. $resp = $this->_sendcmd('online' . ($prefixes ? ' ' . join(' ', $prefixes) : ''));
  127. if (!strlen(trim($resp)))
  128. return array();
  129. // Parse the result and trim namespace.
  130. $result = array();
  131. foreach (explode("\n", $resp) as $line)
  132. {
  133. @list ($id, $counter) = explode(" ", $line);
  134. if (!strlen($id))
  135. continue;
  136. if (strlen($this->_namespace) && strpos($id, $this->_namespace) === 0)
  137. $id = substr($id, strlen($this->_namespace));
  138. $result[$id] = $counter;
  139. }
  140. return $result;
  141. }
  142. /**
  143. * Return list of online channels IDs.
  144. *
  145. * Example usage:
  146. * ~~~ Get the list of all listened channels.
  147. * $list = Realplexor::instance()->online();
  148. * ~~~ Get the list of online channels which names are started with "id_" only.
  149. * $list = Realplexor::instance()->(array('id_'));
  150. *
  151. * @param array $prefixes If set, only online IDs with these prefixes are returned.
  152. * @return array List of matched online IDs.
  153. */
  154. public function online($prefixes = null)
  155. {
  156. return array_keys($this->onlinecounters($prefixes));
  157. }
  158. /**
  159. * Return all Realplexor events (e.g. ID offline/offline changes)
  160. * happened after $from cursor.
  161. *
  162. * @param string $from Start watching from this cursor.
  163. * @param array $prefixes Watch only changes of IDs with these prefixes.
  164. * @return array List of array("event" => ..., "cursor" => ..., "id" => ...).
  165. */
  166. public function watch($from = 0, $prefixes = null)
  167. {
  168. $prefixes = $prefixes !== null ? (array) $prefixes : array();
  169. if (!preg_match('/^[\d.]+$/', $from))
  170. throw new Exception("Position value must be numeric, \"$from\" given");
  171. // Add namespaces.
  172. if (strlen($this->_namespace))
  173. {
  174. if (!$prefixes)
  175. $prefixes = array(''); // if no prefix passed, we still need namespace prefix
  176. foreach ($prefixes as $i => $idp) {
  177. $prefixes[$i] = $this->_namespace . $idp;
  178. }
  179. }
  180. // Execute.
  181. $resp = $this->_sendcmd("watch $from" . ($prefixes? " " . join(" ", $prefixes) : ""));
  182. if (!trim($resp))
  183. return array();
  184. $resp = explode("\n", trim($resp));
  185. // Parse.
  186. $events = array();
  187. foreach ($resp as $line)
  188. {
  189. if (!preg_match('/^ (\w+) \s+ ([^:]+):(\S+) \s* $/sx', $line, $m))
  190. {
  191. trigger_error("Cannot parse the event: \"$line\"");
  192. continue;
  193. }
  194. list ($event, $pos, $id) = array($m[1], $m[2], $m[3]);
  195. // Cut off namespace.
  196. if ($from && strlen($this->_namespace) && strpos($id, $this->_namespace) === 0)
  197. $id = substr($id, strlen($this->_namespace));
  198. $events[] = array(
  199. 'event' => $event,
  200. 'pos' => $pos,
  201. 'id' => $id,
  202. );
  203. }
  204. return $events;
  205. }
  206. /**
  207. * Send IN command.
  208. *
  209. * @param string $cmd Command to send.
  210. * @return string Server IN response.
  211. */
  212. private function _sendcmd($cmd)
  213. {
  214. return $this->_send(null, "$cmd\n");
  215. }
  216. /**
  217. * Send specified data to IN channel. Return response data.
  218. * Throw Exception in case of error.
  219. *
  220. * @param string $identifier If set, pass this identifier string.
  221. * @param string $body Data to be sent.
  222. * @return string Response from IN line.
  223. */
  224. private function _send($identifier, $body)
  225. {
  226. // Build HTTP request.
  227. $headers = "X-Realplexor: {$this->_config['identifier']}="
  228. . ($this->_config['login'] ? $this->_config['login'] . ":" . $this->_config['password'] . '@' : '')
  229. . ($identifier? $identifier : "")
  230. . "\r\n";
  231. $data = ""
  232. . "POST / HTTP/1.1\r\n"
  233. . "Host: " . $this->_config['host'] . "\r\n"
  234. . "Content-Length: " . $this->_strlen($body) . "\r\n"
  235. . $headers
  236. . "\r\n"
  237. . $body;
  238. // Proceed with sending.
  239. $old = ini_get('track_errors');
  240. ini_set('track_errors', 1);
  241. $result = null;
  242. try {
  243. $host = $this->_config['port'] == 443 ? "ssl://" . $this->_config['host'] : $this->_config['host'];
  244. $fs = @fsockopen($host, $this->_config['port'], $errno, $errstr, $this->_config['timeout']);
  245. if (!$fs)
  246. throw new Exception("Error #$errno: $errstr");
  247. if (@fwrite($fs, $data) === false)
  248. throw new Exception($php_errormsg);
  249. if (!@stream_socket_shutdown($fs, STREAM_SHUT_WR))
  250. {
  251. throw new Exception($php_errormsg);
  252. break;
  253. }
  254. $result = @stream_get_contents($fs);
  255. if ($result === false)
  256. throw new Exception($php_errormsg);
  257. if (!@fclose($fs))
  258. throw new Exception($php_errormsg);
  259. ini_set('track_errors', $old);
  260. } catch (Exception $e) {
  261. ini_set('track_errors', $old);
  262. throw $e;
  263. }
  264. // Analyze the result.
  265. if ($result)
  266. {
  267. @list ($headers, $body) = preg_split('/\r?\n\r?\n/s', $result, 2);
  268. if (!preg_match('{^HTTP/[\d.]+ \s+ ((\d+) [^\r\n]*)}six', $headers, $m))
  269. throw new Exception("Non-HTTP response received:\n" . $result);
  270. if ($m[2] != 200)
  271. throw new Exception("Request failed: " . $m[1] . "\n" . $body);
  272. if (!preg_match('/^Content-Length: \s* (\d+)/mix', $headers, $m))
  273. throw new Exception("No Content-Length header in response headers:\n" . $headers);
  274. $needLen = $m[1];
  275. $recvLen = $this->_strlen($body);
  276. if ($needLen != $recvLen)
  277. throw new Exception("Response length ($recvLen) is different than specified in Content-Length header ($needLen): possibly broken response\n");
  278. return $body;
  279. }
  280. return $result;
  281. }
  282. /**
  283. * Wrapper for mbstring-overloaded strlen().
  284. *
  285. * @param string $body
  286. * @return int
  287. */
  288. private function _strlen($body)
  289. {
  290. return function_exists('mb_orig_strlen')? mb_orig_strlen($body) : strlen($body);
  291. }
  292. }