Project

General

Profile

Paste
Download (11.5 KB) Statistics
| Branch: | Revision:

root / drupal7 / sites / all / modules / feeds / libraries / PuSHSubscriber.inc @ 2c8c2b87

1
<?php
2

    
3
/**
4
 * @file
5
 * Pubsubhubbub subscriber library.
6
 *
7
 * Readme
8
 * http://github.com/lxbarth/PuSHSubscriber
9
 *
10
 * License
11
 * http://github.com/lxbarth/PuSHSubscriber/blob/master/LICENSE.txt
12
 */
13

    
14
/**
15
 * PubSubHubbub subscriber.
16
 */
17
class PuSHSubscriber {
18
  protected $domain;
19
  protected $subscriber_id;
20
  protected $subscription_class;
21
  protected $env;
22

    
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
  /**
48
   * Protect constructor.
49
   */
50
  protected function __construct($domain, $subscriber_id, $subscription_class, PuSHSubscriberEnvironmentInterface $env) {
51
    $this->domain = $domain;
52
    $this->subscriber_id = $subscriber_id;
53
    $this->subscription_class = $subscription_class;
54
    $this->env = $env;
55
  }
56

    
57
  /**
58
   * Subscribe to a given URL. Attempt to retrieve 'hub' and 'self' links from
59
   * document at $url and issue a subscription request to the hub.
60
   *
61
   * @param $url
62
   *   The URL of the feed to subscribe to.
63
   * @param $callback_url
64
   *   The full URL that hub should invoke for subscription verification or for
65
   *   notifications.
66
   * @param $hub
67
   *   The URL of a hub. If given overrides the hub URL found in the document
68
   *   at $url.
69
   */
70
  public function subscribe($url, $callback_url, $hub = '') {
71
    // Fetch document, find rel=hub and rel=self.
72
    // If present, issue subscription request.
73
    $request = curl_init($url);
74
    curl_setopt($request, CURLOPT_FOLLOWLOCATION, TRUE);
75
    curl_setopt($request, CURLOPT_RETURNTRANSFER, TRUE);
76
    $data = curl_exec($request);
77
    if (curl_getinfo($request, CURLINFO_HTTP_CODE) == 200) {
78
      try {
79
        $xml = @ new SimpleXMLElement($data);
80
        $xml->registerXPathNamespace('atom', 'http://www.w3.org/2005/Atom');
81
        if (empty($hub) && $hub = @current($xml->xpath("//atom:link[attribute::rel='hub']"))) {
82
          $hub = (string) $hub->attributes()->href;
83
        }
84
        if ($self = @current($xml->xpath("//atom:link[attribute::rel='self']"))) {
85
          $self = (string) $self->attributes()->href;
86
        }
87
      }
88
      catch (Exception $e) {
89
        // Do nothing.
90
      }
91
    }
92
    curl_close($request);
93
    // Fall back to $url if $self is not given.
94
    if (!$self) {
95
      $self = $url;
96
    }
97
    if (!empty($hub) && !empty($self)) {
98
      $this->request($hub, $self, 'subscribe', $callback_url);
99
    }
100
  }
101

    
102
  /**
103
   * @todo Unsubscribe from a hub.
104
   * @todo Make sure we unsubscribe with the correct topic URL as it can differ
105
   * from the initial subscription URL.
106
   *
107
   * @param $topic_url
108
   *   The URL of the topic to unsubscribe from.
109
   * @param $callback_url
110
   *   The callback to unsubscribe.
111
   */
112
  public function unsubscribe($topic_url, $callback_url) {
113
    if ($sub = $this->subscription()) {
114
      $this->request($sub->hub, $sub->topic, 'unsubscribe', $callback_url);
115
      $sub->delete();
116
    }
117
  }
118

    
119
  /**
120
   * Request handler for subscription callbacks.
121
   */
122
  public function handleRequest($callback) {
123
    if (isset($_GET['hub_challenge'])) {
124
      $this->verifyRequest();
125
    }
126
    // No subscription notification has ben sent, we are being notified.
127
    else {
128
      if ($raw = $this->receive()) {
129
        $callback($raw, $this->domain, $this->subscriber_id);
130
      }
131
    }
132
  }
133

    
134
  /**
135
   * Receive a notification.
136
   *
137
   * @param $ignore_signature
138
   *   If FALSE, only accept payload if there is a signature present and the
139
   *   signature matches the payload. Warning: setting to TRUE results in
140
   *   unsafe behavior.
141
   *
142
   * @return
143
   *   An XML string that is the payload of the notification if valid, FALSE
144
   *   otherwise.
145
   */
146
  public function receive($ignore_signature = FALSE) {
147
    /**
148
     * Verification steps:
149
     *
150
     * 1) Verify that this is indeed a POST reuest.
151
     * 2) Verify that posted string is XML.
152
     * 3) Per default verify sender of message by checking the message's
153
     *    signature against the shared secret.
154
     */
155
    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
156
      $raw = file_get_contents('php://input');
157
      if (@simplexml_load_string($raw)) {
158
        if ($ignore_signature) {
159
          return $raw;
160
        }
161
        if (isset($_SERVER['HTTP_X_HUB_SIGNATURE']) && ($sub = $this->subscription())) {
162
          $result = array();
163
          parse_str($_SERVER['HTTP_X_HUB_SIGNATURE'], $result);
164
          if (isset($result['sha1']) && $result['sha1'] === hash_hmac('sha1', $raw, $sub->secret)) {
165
            return $raw;
166
          }
167
          else {
168
            $this->log('Could not verify signature.', 'error');
169
          }
170
        }
171
        else {
172
          $this->log('No signature present.', 'error');
173
        }
174
      }
175
    }
176
    return FALSE;
177
  }
178

    
179
  /**
180
   * Verify a request. After a hub has received a subscribe or unsubscribe
181
   * request (see PuSHSubscriber::request()) it sends back a challenge verifying
182
   * that an action indeed was requested ($_GET['hub_challenge']). This
183
   * method handles the challenge.
184
   */
185
  public function verifyRequest() {
186
    if (!isset($_GET['hub_challenge'])) {
187
      return $this->rejectRequest();
188
    }
189

    
190
    // Don't accept modes of 'subscribed' or 'unsubscribed'.
191
    if ($_GET['hub_mode'] !== 'subscribe' && $_GET['hub_mode'] !== 'unsubscribe') {
192
      return $this->rejectRequest();
193
    }
194

    
195
    // No available subscription.
196
    if (!$sub = $this->subscription()) {
197
      return $this->rejectRequest();
198
    }
199

    
200
    // Not what we asked for.
201
    if ($_GET['hub_mode'] !== $sub->status) {
202
      return $this->rejectRequest();
203
    }
204

    
205
    // Wrong URL.
206
    if ($_GET['hub_topic'] !== $sub->topic) {
207
      return $this->rejectRequest();
208
    }
209

    
210
    if ($sub->status === 'subscribe') {
211
      $sub->status = 'subscribed';
212
      $this->log('Verified "subscribe" request.');
213
    }
214
    else {
215
      $sub->status = 'unsubscribed';
216
      $this->log('Verified "unsubscribe" request.');
217
    }
218

    
219
    $sub->post_fields = array();
220
    $sub->save();
221

    
222
    header('HTTP/1.1 200 "Found"', NULL, 200);
223
    print check_plain($_GET['hub_challenge']);
224
    drupal_exit();
225
  }
226

    
227
  /**
228
   * Issue a subscribe or unsubcribe request to a PubsubHubbub hub.
229
   *
230
   * @param $hub
231
   *   The URL of the hub's subscription endpoint.
232
   * @param $topic
233
   *   The topic URL of the feed to subscribe to.
234
   * @param $mode
235
   *   'subscribe' or 'unsubscribe'.
236
   * @param $callback_url
237
   *   The subscriber's notifications callback URL.
238
   *
239
   * Compare to http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.2.html#anchor5
240
   *
241
   * @todo Make concurrency safe.
242
   */
243
  protected function request($hub, $topic, $mode, $callback_url) {
244
    $secret = drupal_random_key(40);
245
    $post_fields = array(
246
      'hub.callback' => $callback_url,
247
      'hub.mode' => $mode,
248
      'hub.topic' => $topic,
249
      'hub.verify' => 'sync',
250
      'hub.lease_seconds' => '', // Permanent subscription.
251
      'hub.secret' => $secret,
252
    );
253
    $sub = new $this->subscription_class($this->domain, $this->subscriber_id, $hub, $topic, $secret, $mode, $post_fields);
254
    $sub->save();
255
    // Issue subscription request.
256
    $request = curl_init($hub);
257
    curl_setopt($request, CURLOPT_POST, TRUE);
258
    curl_setopt($request, CURLOPT_POSTFIELDS, $post_fields);
259
    curl_setopt($request, CURLOPT_RETURNTRANSFER, TRUE);
260
    curl_exec($request);
261
    $code = curl_getinfo($request, CURLINFO_HTTP_CODE);
262
    if (in_array($code, array(202, 204))) {
263
      $this->log("Positive response to \"$mode\" request ($code).");
264
    }
265
    else {
266
      $sub->status = $mode . ' failed';
267
      $sub->save();
268
      $this->log("Error issuing \"$mode\" request to $hub ($code).", 'error');
269
    }
270
    curl_close($request);
271
  }
272

    
273
  /**
274
   * Get the subscription associated with this subscriber.
275
   *
276
   * @return
277
   *   A PuSHSubscriptionInterface object if a subscription exist, NULL
278
   *   otherwise.
279
   */
280
  public function subscription() {
281
    return call_user_func(array($this->subscription_class, 'load'), $this->domain, $this->subscriber_id);
282
  }
283

    
284
  /**
285
   * Determine whether this subscriber is successfully subscribed or not.
286
   */
287
  public function subscribed() {
288
    if ($sub = $this->subscription()) {
289
      if ($sub->status == 'subscribed') {
290
        return TRUE;
291
      }
292
    }
293
    return FALSE;
294
  }
295

    
296
  /**
297
   * Helper for messaging.
298
   */
299
  protected function msg($msg, $level = 'status') {
300
    $this->env->msg($msg, $level);
301
  }
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
   * Rejects a request subscription request.
312
   */
313
  protected function rejectRequest() {
314
    header('HTTP/1.1 404 "Not Found"', NULL, 404);
315
    $this->log('Could not verify subscription.', 'error');
316
    drupal_exit();
317
  }
318

    
319
}
320

    
321
/**
322
 * Implement to provide a storage backend for subscriptions.
323
 *
324
 * Variables passed in to the constructor must be accessible as public class
325
 * variables.
326
 */
327
interface PuSHSubscriptionInterface {
328
  /**
329
   * @param $domain
330
   *   A string that defines the domain in which the subscriber_id is unique.
331
   * @param $subscriber_id
332
   *   A unique numeric subscriber id.
333
   * @param $hub
334
   *   The URL of the hub endpoint.
335
   * @param $topic
336
   *   The topic to subscribe to.
337
   * @param $secret
338
   *   A secret key used for message authentication.
339
   * @param $status
340
   *   The status of the subscription.
341
   *   'subscribe' - subscribing to a feed.
342
   *   'unsubscribe' - unsubscribing from a feed.
343
   *   'subscribed' - subscribed.
344
   *   'unsubscribed' - unsubscribed.
345
   *   'subscribe failed' - subscribe request failed.
346
   *   'unsubscribe failed' - unsubscribe request failed.
347
   * @param $post_fields
348
   *   An array of the fields posted to the hub.
349
   */
350
  public function __construct($domain, $subscriber_id, $hub, $topic, $secret, $status = '', $post_fields = '');
351

    
352
  /**
353
   * Save a subscription.
354
   */
355
  public function save();
356

    
357
  /**
358
   * Load a subscription.
359
   *
360
   * @return
361
   *   A PuSHSubscriptionInterface object if a subscription exist, NULL
362
   *   otherwise.
363
   */
364
  public static function load($domain, $subscriber_id);
365

    
366
  /**
367
   * Delete a subscription.
368
   */
369
  public function delete();
370
}
371

    
372
/**
373
 * Implement to provide environmental functionality like user messages and
374
 * logging.
375
 */
376
interface PuSHSubscriberEnvironmentInterface {
377
  /**
378
   * A message to be displayed to the user on the current page load.
379
   *
380
   * @param $msg
381
   *   A string that is the message to be displayed.
382
   * @param $level
383
   *   A string that is either 'status', 'warning' or 'error'.
384
   */
385
  public function msg($msg, $level = 'status');
386

    
387
  /**
388
   * A log message to be logged to the database or the file system.
389
   *
390
   * @param $msg
391
   *   A string that is the message to be displayed.
392
   * @param $level
393
   *   A string that is either 'status', 'warning' or 'error'.
394
   */
395
  public function log($msg, $level = 'status');
396
}