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
|
}
|