Projet

Général

Profil

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

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

1
<?php
2

    
3
/**
4
 * @file
5
 * Pubsubhubbub subscriber library.
6
 *
7
 * Readme
8
 * @see http://github.com/lxbarth/PuSHSubscriber
9
 *
10
 * License
11
 * @see 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
   * Compare to http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.2.html#anchor5
231
   *
232
   * @param $hub
233
   *   The URL of the hub's subscription endpoint.
234
   * @param $topic
235
   *   The topic URL of the feed to subscribe to.
236
   * @param $mode
237
   *   'subscribe' or 'unsubscribe'.
238
   * @param $callback_url
239
   *   The subscriber's notifications callback URL.
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
      // Permanent subscription.
251
      'hub.lease_seconds' => '',
252
      'hub.secret' => $secret,
253
    );
254
    $sub = new $this->subscription_class($this->domain, $this->subscriber_id, $hub, $topic, $secret, $mode, $post_fields);
255
    $sub->save();
256
    // Issue subscription request.
257
    $request = curl_init($hub);
258
    curl_setopt($request, CURLOPT_POST, TRUE);
259
    curl_setopt($request, CURLOPT_POSTFIELDS, $post_fields);
260
    curl_setopt($request, CURLOPT_RETURNTRANSFER, TRUE);
261
    curl_exec($request);
262
    $code = curl_getinfo($request, CURLINFO_HTTP_CODE);
263
    if (in_array($code, array(202, 204))) {
264
      $this->log("Positive response to \"$mode\" request ($code).");
265
    }
266
    else {
267
      $sub->status = $mode . ' failed';
268
      $sub->save();
269
      $this->log("Error issuing \"$mode\" request to $hub ($code).", 'error');
270
    }
271
    curl_close($request);
272
  }
273

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

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

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

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

    
311
  /**
312
   * Rejects a request subscription request.
313
   */
314
  protected function rejectRequest() {
315
    header('HTTP/1.1 404 "Not Found"', NULL, 404);
316
    $this->log('Could not verify subscription.', 'error');
317
    drupal_exit();
318
  }
319

    
320
}
321

    
322
/**
323
 * Implement to provide a storage backend for subscriptions.
324
 *
325
 * Variables passed in to the constructor must be accessible as public class
326
 * variables.
327
 */
328
interface PuSHSubscriptionInterface {
329

    
330
  /**
331
   * @param $domain
332
   *   A string that defines the domain in which the subscriber_id is unique.
333
   * @param $subscriber_id
334
   *   A unique numeric subscriber id.
335
   * @param $hub
336
   *   The URL of the hub endpoint.
337
   * @param $topic
338
   *   The topic to subscribe to.
339
   * @param $secret
340
   *   A secret key used for message authentication.
341
   * @param $status
342
   *   The status of the subscription.
343
   *   'subscribe' - subscribing to a feed.
344
   *   'unsubscribe' - unsubscribing from a feed.
345
   *   'subscribed' - subscribed.
346
   *   'unsubscribed' - unsubscribed.
347
   *   'subscribe failed' - subscribe request failed.
348
   *   'unsubscribe failed' - unsubscribe request failed.
349
   * @param $post_fields
350
   *   An array of the fields posted to the hub.
351
   */
352
  public function __construct($domain, $subscriber_id, $hub, $topic, $secret, $status = '', $post_fields = '');
353

    
354
  /**
355
   * Save a subscription.
356
   */
357
  public function save();
358

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

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

    
373
}
374

    
375
/**
376
 * Implement to provide environmental functionality like user messages and
377
 * logging.
378
 */
379
interface PuSHSubscriberEnvironmentInterface {
380

    
381
  /**
382
   * A message to be displayed to the user on the current page load.
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 msg($msg, $level = 'status');
390

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

    
401
}