Projet

Général

Profil

Paste
Télécharger (11,8 ko) Statistiques
| Branche: | Révision:

root / drupal7 / sites / all / modules / feeds / libraries / PuSHSubscriber.inc @ 13755f8d

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
      /**
188
       * If a subscription is present, compare the verify token. If the token
189
       * matches, set the status on the subscription record and confirm
190
       * positive.
191
       *
192
       * If we cannot find a matching subscription and the hub checks on
193
       * 'unsubscribe' confirm positive.
194
       *
195
       * In all other cases confirm negative.
196
       */
197
      if ($sub = $this->subscription()) {
198
        if ($_GET['hub_verify_token'] == $sub->post_fields['hub.verify_token']) {
199
          if ($_GET['hub_mode'] == 'subscribe' && $sub->status == 'subscribe') {
200
            $sub->status = 'subscribed';
201
            $sub->post_fields = array();
202
            $sub->save();
203
            $this->log('Verified "subscribe" request.');
204
            $verify = TRUE;
205
          }
206
          elseif ($_GET['hub_mode'] == 'unsubscribe' && $sub->status == 'unsubscribe') {
207
            $sub->status = 'unsubscribed';
208
            $sub->post_fields = array();
209
            $sub->save();
210
            $this->log('Verified "unsubscribe" request.');
211
            $verify = TRUE;
212
          }
213
        }
214
      }
215
      elseif ($_GET['hub_mode'] == 'unsubscribe') {
216
        $this->log('Verified "unsubscribe" request.');
217
        $verify = TRUE;
218
      }
219
      if ($verify) {
220
        header('HTTP/1.1 200 "Found"', NULL, 200);
221
        print $_GET['hub_challenge'];
222
        drupal_exit();
223
      }
224
    }
225
    header('HTTP/1.1 404 "Not Found"', NULL, 404);
226
    $this->log('Could not verify subscription.', 'error');
227
    drupal_exit();
228
  }
229

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

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

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

    
300
  /**
301
   * Helper for messaging.
302
   */
303
  protected function msg($msg, $level = 'status') {
304
    $this->env->msg($msg, $level);
305
  }
306

    
307
  /**
308
   * Helper for logging.
309
   */
310
  protected function log($msg, $level = 'status') {
311
    $this->env->log("{$this->domain}:{$this->subscriber_id}\t$msg", $level);
312
  }
313
}
314

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

    
346
  /**
347
   * Save a subscription.
348
   */
349
  public function save();
350

    
351
  /**
352
   * Load a subscription.
353
   *
354
   * @return
355
   *   A PuSHSubscriptionInterface object if a subscription exist, NULL
356
   *   otherwise.
357
   */
358
  public static function load($domain, $subscriber_id);
359

    
360
  /**
361
   * Delete a subscription.
362
   */
363
  public function delete();
364
}
365

    
366
/**
367
 * Implement to provide environmental functionality like user messages and
368
 * logging.
369
 */
370
interface PuSHSubscriberEnvironmentInterface {
371
  /**
372
   * A message to be displayed to the user on the current page load.
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 msg($msg, $level = 'status');
380

    
381
  /**
382
   * A log message to be logged to the database or the file system.
383
   *
384
   * @param $msg
385
   *   A string that is the message to be displayed.
386
   * @param $level
387
   *   A string that is either 'status', 'warning' or 'error'.
388
   */
389
  public function log($msg, $level = 'status');
390
}