PageRenderTime 23ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/elgg/mod/pshb/lib/PuSHSubscriber.inc

https://bitbucket.org/rhizomatik/lorea_production/
Pascal | 401 lines | 158 code | 16 blank | 227 comment | 28 complexity | ba7a1350054b0ed2dd81b32110eac2da MD5 | raw file
Possible License(s): GPL-3.0, GPL-2.0, BSD-3-Clause, LGPL-2.1
  1. <?php
  2. /**
  3. * @file
  4. * Pubsubhubbub subscriber library.
  5. *
  6. * Readme
  7. * http://github.com/lxbarth/PuSHSubscriber
  8. *
  9. * License
  10. * http://github.com/lxbarth/PuSHSubscriber/blob/master/LICENSE.txt
  11. */
  12. /**
  13. * PubSubHubbub subscriber.
  14. */
  15. function PuSH_GET($var) {
  16. return get_input($var);
  17. }
  18. class PuSHSubscriber {
  19. protected $domain;
  20. protected $subscriber_id;
  21. protected $subscription_class;
  22. protected $env;
  23. /**
  24. * Singleton.
  25. *
  26. * PuSHSubscriber identifies a unique subscription by a domain and a numeric
  27. * id. The numeric id is assumed to e unique in its domain.
  28. *
  29. * @param $domain
  30. * A string that identifies the domain in which $subscriber_id is unique.
  31. * @param $subscriber_id
  32. * A numeric subscriber id.
  33. * @param $subscription_class
  34. * The class to use for handling subscriptions. Class MUST implement
  35. * PuSHSubscriberSubscriptionInterface
  36. * @param PuSHSubscriberEnvironmentInterface $env
  37. * Environmental object for messaging and logging.
  38. */
  39. public static function instance($domain, $subscriber_id, $subscription_class, PuSHSubscriberEnvironmentInterface $env) {
  40. static $subscribers;
  41. if (!isset($subscriber[$domain][$subscriber_id])) {
  42. $subscriber = new PuSHSubscriber($domain, $subscriber_id, $subscription_class, $env);
  43. }
  44. return $subscriber;
  45. }
  46. /**
  47. * Protect constructor.
  48. */
  49. protected function __construct($domain, $subscriber_id, $subscription_class, PuSHSubscriberEnvironmentInterface $env) {
  50. $this->domain = $domain;
  51. $this->subscriber_id = $subscriber_id;
  52. $this->subscription_class = $subscription_class;
  53. $this->env = $env;
  54. }
  55. /**
  56. * Subscribe to a given URL. Attempt to retrieve 'hub' and 'self' links from
  57. * document at $url and issue a subscription request to the hub.
  58. *
  59. * @param $url
  60. * The URL of the feed to subscribe to.
  61. * @param $callback_url
  62. * The full URL that hub should invoke for subscription verification or for
  63. * notifications.
  64. * @param $hub
  65. * The URL of a hub. If given overrides the hub URL found in the document
  66. * at $url.
  67. */
  68. public function subscribe($url, $callback_url, $hub = '') {
  69. // Fetch document, find rel=hub and rel=self.
  70. // If present, issue subscription request.
  71. $request = curl_init($url);
  72. curl_setopt($request, CURLOPT_FOLLOWLOCATION, TRUE);
  73. curl_setopt($request, CURLOPT_RETURNTRANSFER, TRUE);
  74. if ((bool)preg_match('/^https:\/\//i', $url)) {
  75. // XXX unhibit ssl verifyier for now...
  76. curl_setopt($request, CURLOPT_SSL_VERIFYPEER, 0);
  77. }
  78. $data = curl_exec($request);
  79. $ret_code = curl_getinfo($request, CURLINFO_HTTP_CODE);
  80. $xml = true;
  81. if ($ret_code == 200 || $ret_code == 302) {
  82. try {
  83. $xml = @ new SimpleXMLElement($data);
  84. $xml->registerXPathNamespace('atom', 'http://www.w3.org/2005/Atom');
  85. if (empty($hub) && $hub = @current($xml->xpath("//atom:link[attribute::rel='hub']"))) {
  86. $hub = (string) $hub->attributes()->href;
  87. }
  88. if ($self = @current($xml->xpath("//atom:link[attribute::rel='self']"))) {
  89. $self = (string) $self->attributes()->href;
  90. }
  91. }
  92. catch (Exception $e) {}
  93. }
  94. curl_close($request);
  95. // Fall back to $url if $self is not given.
  96. if (!$self) {
  97. $self = $url;
  98. }
  99. if (!empty($hub) && !empty($self)) {
  100. $this->request($hub, $self, 'subscribe', $callback_url);
  101. return $xml;
  102. }
  103. return false;
  104. }
  105. /**
  106. * @todo Unsubscribe from a hub.
  107. * @todo Make sure we unsubscribe with the correct topic URL as it can differ
  108. * from the initial subscription URL.
  109. *
  110. * @param $topic_url
  111. * The URL of the topic to unsubscribe from.
  112. * @param $callback_url
  113. * The callback to unsubscribe.
  114. */
  115. public function unsubscribe($topic_url, $callback_url) {
  116. if ($sub = $this->loadSubscription()) {
  117. $this->request($sub->hub, $sub->topic, 'unsubscribe', $callback_url);
  118. $sub->delete();
  119. }
  120. }
  121. /**
  122. * Request handler for subscription callbacks.
  123. */
  124. public function handleRequest($callback) {
  125. /*error_log("SUBSCRIBE");
  126. error_log(PuSH_GET('hub_challenge'));
  127. error_log(get_input('hub.challenge'));
  128. error_log(get_input('hub_challenge'));
  129. error_log(PuSH_GET('hub_challenge'));*/
  130. if (PuSH_GET('hub_challenge')) {
  131. $this->verifyRequest();
  132. }
  133. // No subscription notification has ben sent, we are being notified.
  134. else {
  135. if ($raw = $this->receive()) {
  136. $callback($raw, $this->domain, $this->subscriber_id);
  137. }
  138. }
  139. }
  140. /**
  141. * Receive a notification.
  142. *
  143. * @param $ignore_signature
  144. * If FALSE, only accept payload if there is a signature present and the
  145. * signature matches the payload. Warning: setting to TRUE results in
  146. * unsafe behavior.
  147. *
  148. * @return
  149. * An XML string that is the payload of the notification if valid, FALSE
  150. * otherwise.
  151. */
  152. public function receive($ignore_signature = FALSE) {
  153. /**
  154. * Verification steps:
  155. *
  156. * 1) Verify that this is indeed a POST reuest.
  157. * 2) Verify that posted string is XML.
  158. * 3) Per default verify sender of message by checking the message's
  159. * signature against the shared secret.
  160. */
  161. if ($_SERVER['REQUEST_METHOD'] == 'POST') {
  162. $raw = file_get_contents('php://input');
  163. //error_log("raw".$raw);
  164. if (@simplexml_load_string($raw)) {
  165. if ($ignore_signature) {
  166. return $raw;
  167. }
  168. if (isset($_SERVER['HTTP_X_HUB_SIGNATURE']) && ($sub = $this->loadSubscription())) {
  169. $result = array();
  170. // error_log("pshb: ".$_SERVER['HTTP_X_HUB_SIGNATURE']);
  171. parse_str($_SERVER['HTTP_X_HUB_SIGNATURE'], $result);
  172. if (isset($result['sha1']) && $result['sha1'] == hash_hmac('sha1', $raw, $sub->secret)) {
  173. return $raw;
  174. }
  175. else {
  176. $this->log('Could not verify signature.', 'error');
  177. }
  178. }
  179. else {
  180. $this->log('No signature present.', 'error');
  181. }
  182. }
  183. }
  184. return FALSE;
  185. }
  186. /**
  187. * Verify a request. After a hub has received a subscribe or unsubscribe
  188. * request (see PuSHSubscriber::request()) it sends back a challenge verifying
  189. * that an action indeed was requested ($_GET['hub_challenge']). This
  190. * method handles the challenge.
  191. */
  192. public function verifyRequest() {
  193. if (PuSH_GET('hub_challenge')) {
  194. /**
  195. * If a subscription is present, compare the verify token. If the token
  196. * matches, set the status on the subscription record and confirm
  197. * positive.
  198. *
  199. * If we cannot find a matching subscription and the hub checks on
  200. * 'unsubscribe' confirm positive.
  201. *
  202. * In all other cases confirm negative.
  203. */
  204. // error_log("verify request");
  205. if ($sub = $this->loadSubscription()) {
  206. // error_log("blah".PuSH_GET('hub_verify_token').":".$sub->post_fields['hub.verify_token']);
  207. if (PuSH_GET('hub_verify_token') == $sub->post_fields['hub.verify_token']) {
  208. if (PuSH_GET('hub_mode') == 'subscribe' && $sub->status == 'subscribe') {
  209. $sub->status = 'subscribed';
  210. $sub->post_fields = array();
  211. $sub->save();
  212. $this->log('Verified "subscribe" request.');
  213. $verify = TRUE;
  214. }
  215. elseif (PuSH_GET('hub_mode') == 'unsubscribe' && $sub->status == 'unsubscribe') {
  216. $sub->status = 'unsubscribed';
  217. $sub->post_fields = array();
  218. $sub->save();
  219. $this->log('Verified "unsubscribe" request.');
  220. $verify = TRUE;
  221. }
  222. }
  223. }
  224. elseif (PuSH_GET('hub_mode') == 'unsubscribe') {
  225. $this->log('Verified "unsubscribe" request.');
  226. $verify = TRUE;
  227. }
  228. if ($verify) {
  229. header('HTTP/1.1 200 "Found"', null, 200);
  230. print PuSH_GET('hub_challenge');
  231. exit();
  232. }
  233. }
  234. header('HTTP/1.1 404 "Not Found"', null, 404);
  235. $this->log('Could not verify subscription.', 'error');
  236. exit();
  237. }
  238. /**
  239. * Issue a subscribe or unsubcribe request to a PubsubHubbub hub.
  240. *
  241. * @param $hub
  242. * The URL of the hub's subscription endpoint.
  243. * @param $topic
  244. * The topic URL of the feed to subscribe to.
  245. * @param $mode
  246. * 'subscribe' or 'unsubscribe'.
  247. * @param $callback_url
  248. * The subscriber's notifications callback URL.
  249. *
  250. * Compare to http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.2.html#anchor5
  251. *
  252. * @todo Make concurrency safe.
  253. */
  254. protected function request($hub, $topic, $mode, $callback_url) {
  255. $secret = hash('sha1', uniqid(rand(), true));
  256. // error_log($callback_url);
  257. $post_fields = array(
  258. 'hub.callback' => $callback_url,
  259. 'hub.mode' => $mode,
  260. 'hub.topic' => $topic,
  261. 'hub.verify' => 'sync',
  262. 'hub.lease_seconds' => '', // Permanent subscription.
  263. 'hub.secret' => $secret,
  264. 'hub.verify_token' => md5(session_id() . rand()),
  265. );
  266. $sub = new $this->subscription_class($this->domain, $this->subscriber_id, $hub, $topic, $secret, $mode, $post_fields);
  267. $sub->save();
  268. // Issue subscription request.
  269. $request = curl_init($hub);
  270. curl_setopt($request, CURLOPT_POST, TRUE);
  271. curl_setopt($request, CURLOPT_POSTFIELDS, $post_fields);
  272. curl_setopt($request, CURLOPT_RETURNTRANSFER, TRUE);
  273. if ((bool)preg_match('/^https:\/\//i', $hub)) {
  274. // XXX unhibit ssl verifyier for now...
  275. curl_setopt($request, CURLOPT_SSL_VERIFYPEER, 0);
  276. }
  277. $data = curl_exec($request);
  278. $code = curl_getinfo($request, CURLINFO_HTTP_CODE);
  279. // error_log($data);
  280. // error_log($code);
  281. if (in_array($code, array(202, 204))) {
  282. $this->log("Positive response to \"$mode\" request ($code).");
  283. }
  284. else {
  285. $sub->status = $mode .' failed with '.$code;
  286. $sub->save();
  287. $this->log("Error issuing \"$mode\" request to $hub ($code).", 'error');
  288. }
  289. curl_close($request);
  290. }
  291. /**
  292. * Helper for loading a subscription.
  293. */
  294. protected function loadSubscription() {
  295. return call_user_func(array($this->subscription_class, 'load'), $this->domain, $this->subscriber_id);
  296. }
  297. /**
  298. * Helper for messaging.
  299. */
  300. protected function msg($msg, $level = 'status') {
  301. $this->env->msg($msg, $level);
  302. }
  303. /**
  304. * Helper for logging.
  305. */
  306. protected function log($msg, $level = 'status') {
  307. $this->env->log("{$this->domain}:{$this->subscriber_id}\t$msg", $level);
  308. }
  309. }
  310. /**
  311. * Implement to provide a storage backend for subscriptions.
  312. *
  313. * Variables passed in to the constructor must be accessible as public class
  314. * variables.
  315. */
  316. interface PuSHSubscriptionInterface {
  317. /**
  318. * @param $domain
  319. * A string that defines the domain in which the subscriber_id is unique.
  320. * @param $subscriber_id
  321. * A unique numeric subscriber id.
  322. * @param $hub
  323. * The URL of the hub endpoint.
  324. * @param $topic
  325. * The topic to subscribe to.
  326. * @param $secret
  327. * A secret key used for message authentication.
  328. * @param $status
  329. * The status of the subscription.
  330. * 'subscribe' - subscribing to a feed.
  331. * 'unsubscribe' - unsubscribing from a feed.
  332. * 'subscribed' - subscribed.
  333. * 'unsubscribed' - unsubscribed.
  334. * 'subscribe failed' - subscribe request failed.
  335. * 'unsubscribe failed' - unsubscribe request failed.
  336. * @param $post_fields
  337. * An array of the fields posted to the hub.
  338. */
  339. public function __construct($domain, $subscriber_id, $hub, $topic, $secret, $status = '', $post_fields = '');
  340. /**
  341. * Save a subscription.
  342. */
  343. public function save();
  344. /**
  345. * Load a subscription.
  346. *
  347. * @return
  348. * A PuSHSubscriptionInterface object if a subscription exist, NULL
  349. * otherwise.
  350. */
  351. public static function load($domain, $subscriber_id);
  352. /**
  353. * Delete a subscription.
  354. */
  355. public function delete();
  356. }
  357. /**
  358. * Implement to provide environmental functionality like user messages and
  359. * logging.
  360. */
  361. interface PuSHSubscriberEnvironmentInterface {
  362. /**
  363. * A message to be displayed to the user on the current page load.
  364. *
  365. * @param $msg
  366. * A string that is the message to be displayed.
  367. * @param $level
  368. * A string that is either 'status', 'warning' or 'error'.
  369. */
  370. public function msg($msg, $level = 'status');
  371. /**
  372. * A log message to be logged to the database or the file system.
  373. *
  374. * @param $msg
  375. * A string that is the message to be displayed.
  376. * @param $level
  377. * A string that is either 'status', 'warning' or 'error'.
  378. */
  379. public function log($msg, $level = 'status');
  380. }