]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/RSSCloud/RSSCloudRequestNotify.php
Only allow RSSCloud subs to canonical RSS2 profile feeds
[quix0rs-gnu-social.git] / plugins / RSSCloud / RSSCloudRequestNotify.php
1 <?php
2 /**
3  * Action to let RSSCloud aggregators request update notification when
4  * user profile feeds change.
5  *
6  * PHP version 5
7  *
8  * @category Plugin
9  * @package  StatusNet
10  * @author   Zach Copley <zach@status.net>
11  * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
12  * @link     http://status.net/
13  *
14  * StatusNet - the distributed open-source microblogging tool
15  * Copyright (C) 2009, StatusNet, Inc.
16  *
17  * This program is free software: you can redistribute it and/or modify
18  * it under the terms of the GNU Affero General Public License as published by
19  * the Free Software Foundation, either version 3 of the License, or
20  * (at your option) any later version.
21  *
22  * This program is distributed in the hope that it will be useful,
23  * but WITHOUT ANY WARRANTY; without even the implied warranty of
24  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25  * GNU Affero General Public License for more details.
26  *
27  * You should have received a copy of the GNU Affero General Public License
28  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
29  */
30
31 if (!defined('STATUSNET')) {
32     exit(1);
33 }
34
35 /**
36  * Action class to handle RSSCloud notification (subscription) requests
37  *
38  * @category Plugin
39  * @package  StatusNet
40  * @author   Zach Copley <zach@status.net>
41  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
42  * @link     http://status.net/
43  **/
44
45 class RSSCloudRequestNotifyAction extends Action
46 {
47     /**
48      * Initialization.
49      *
50      * @param array $args Web and URL arguments
51      *
52      * @return boolean false if user doesn't exist
53      */
54
55     function prepare($args)
56     {
57         parent::prepare($args);
58
59         $this->ip   = $_SERVER['REMOTE_ADDR'];
60         $this->port = $this->arg('port');
61         $this->path = $this->arg('path');
62
63         if ($this->path[0] != '/') {
64             $this->path = '/' . $this->path;
65         }
66
67         $this->protocol  = $this->arg('protocol');
68         $this->procedure = $this->arg('notifyProcedure');
69         $this->domain    = $this->arg('domain');
70
71         $this->feeds = $this->getFeeds();
72
73         return true;
74     }
75
76     /**
77      * Handle the request
78      *
79      * Checks for all the required parameters for a subscription,
80      * validates that the feed being subscribed to is real, and then
81      * saves the subsctiption.
82      *
83      * @param array $args $_REQUEST data (unused)
84      *
85      * @return void
86      */
87
88     function handle($args)
89     {
90         parent::handle($args);
91
92         if ($_SERVER['REQUEST_METHOD'] != 'POST') {
93             $this->showResult(false, 'Request must be POST.');
94             return;
95         }
96
97         $missing = array();
98
99         if (empty($this->port)) {
100             $missing[] = 'port';
101         }
102
103         if (empty($this->path)) {
104             $missing[] = 'path';
105         }
106
107         if (empty($this->protocol)) {
108             $missing[] = 'protocol';
109         } else if (strtolower($this->protocol) != 'http-post') {
110             $msg = 'Only http-post notifications are supported at this time.';
111             $this->showResult(false, $msg);
112             return;
113         }
114
115         if (!isset($this->procedure)) {
116             $missing[] = 'notifyProcedure';
117         }
118
119         if (!empty($missing)) {
120             $msg = 'The following parameters were missing from the request body: ' .
121                 implode(', ', $missing) . '.';
122             $this->showResult(false, $msg);
123             return;
124         }
125
126         if (empty($this->feeds)) {
127             $msg = 'You must provide at least one valid profile feed url ' .
128               '(url1, url2, url3 ... urlN).';
129             $this->showResult(false, $msg);
130             return;
131         }
132
133         // We have to validate everything before saving anything.
134         // We only return one success or failure no matter how
135         // many feeds the subscriber is trying to subscribe to
136
137         foreach ($this->feeds as $feed) {
138
139             if (!$this->validateFeed($feed)) {
140
141                 $nh = $this->getNotifyUrl();
142                 common_log(LOG_WARNING,
143                            "RSSCloud plugin - $nh tried to subscribe to invalid feed: $feed");
144
145                 $msg = 'Feed subscription failed - Not a valid feed.';
146                 $this->showResult(false, $msg);
147                 return;
148             }
149
150             if (!$this->testNotificationHandler($feed)) {
151                 $msg = 'Feed subscription failed - ' .
152                 'notification handler doesn\'t respond correctly.';
153                 $this->showResult(false, $msg);
154                 return;
155             }
156
157         }
158
159         foreach ($this->feeds as $feed) {
160             $this->saveSubscription($feed);
161         }
162
163         // XXX: What to do about deleting stale subscriptions?
164         // 25 hours seems harsh. WordPress doesn't ever remove
165         // subscriptions.
166
167         $msg = 'Thanks for the subscription. ' .
168           'When the feed(s) update(s) we\'ll notify you.';
169
170         $this->showResult(true, $msg);
171     }
172
173     /**
174      * Validate that the requested feed is one we serve
175      * up via RSSCloud.
176      *
177      * @param string $feed the feed in question
178      *
179      * @return void
180      */
181
182     function validateFeed($feed)
183     {
184         $user = $this->userFromFeed($feed);
185
186         if (empty($user)) {
187             return false;
188         }
189
190         return true;
191     }
192
193     /**
194      * Pull all of the urls (url1, url2, url3...urlN) that
195      * the subscriber wants to subscribe to.
196      *
197      * @return array $feeds the list of feeds
198      */
199
200     function getFeeds()
201     {
202         $feeds = array();
203
204         while (list($key, $feed) = each($this->args)) {
205             if (preg_match('/^url\d*$/', $key)) {
206                 $feeds[] = $feed;
207             }
208         }
209
210         return $feeds;
211     }
212
213     /**
214      * Test that a notification handler is there and is reponding
215      * correctly.  This is called before adding a subscription.
216      *
217      * @param string $feed the feed to verify
218      *
219      * @return boolean success result
220      */
221
222     function testNotificationHandler($feed)
223     {
224         $notifyUrl = $this->getNotifyUrl();
225
226         $notifier = new RSSCloudNotifier();
227
228         if (isset($this->domain)) {
229
230             // 'domain' param set, so we have to use GET and send a challenge
231
232             common_log(LOG_INFO,
233                        'RSSCloud plugin - Testing notification handler with challenge: ' .
234                        $notifyUrl);
235             return $notifier->challenge($notifyUrl, $feed);
236
237         } else {
238             common_log(LOG_INFO, 'RSSCloud plugin - Testing notification handler: ' .
239                        $notifyUrl);
240
241             return $notifier->postUpdate($notifyUrl, $feed);
242         }
243     }
244
245     /**
246      * Build the URL for the notification handler based on the
247      * parameters passed in with the subscription request.
248      *
249      * @return string notification handler url
250      */
251
252     function getNotifyUrl()
253     {
254         if (isset($this->domain)) {
255             return 'http://' . $this->domain . ':' . $this->port . $this->path;
256         } else {
257             return 'http://' . $this->ip . ':' . $this->port . $this->path;
258         }
259     }
260
261     /**
262      * Uses the nickname part of the subscribed feed URL to figure out
263      * whethere there's really a user with such a feed.  Used to
264      * validate feeds before adding a subscription.
265      *
266      * @param string $feed the feed in question
267      *
268      * @return boolean success
269      */
270
271     function userFromFeed($feed)
272     {
273         // We only do canonical RSS2 profile feeds (specified by ID), e.g.:
274         // http://www.example.com/api/statuses/user_timeline/2.rss
275
276         $path  = common_path('api/statuses/user_timeline/');
277         $valid = '%^' . $path . '(?<id>.*)\.rss$%';
278
279         if (preg_match($valid, $feed, $matches)) {
280             $user = User::staticGet('id', $matches['id']);
281             if (!empty($user)) {
282                 return $user;
283             }
284         }
285
286         return false;
287     }
288
289     /**
290      * Save an RSSCloud subscription
291      *
292      * @param string $feed a valid profile feed
293      *
294      * @return boolean success result
295      */
296
297     function saveSubscription($feed)
298     {
299         $user = $this->userFromFeed($feed);
300
301         $notifyUrl = $this->getNotifyUrl();
302
303         $sub = RSSCloudSubscription::getSubscription($user->id, $notifyUrl);
304
305         if ($sub) {
306             common_log(LOG_INFO, "RSSCloud plugin - $notifyUrl refreshed subscription" .
307                          " to user $user->nickname (id: $user->id).");
308         } else {
309
310             $sub = new RSSCloudSubscription();
311
312             $sub->subscribed = $user->id;
313             $sub->url        = $notifyUrl;
314             $sub->created    = common_sql_now();
315
316             if (!$sub->insert()) {
317                 common_log_db_error($sub, 'INSERT', __FILE__);
318                 return false;
319             }
320
321             common_log(LOG_INFO, "RSSCloud plugin - $notifyUrl subscribed" .
322                        " to user $user->nickname (id: $user->id)");
323         }
324
325         return true;
326     }
327
328     /**
329      * Show an XML message indicating the subscription
330      * was successful or failed.
331      *
332      * @param boolean $success whether it was good or bad
333      * @param string  $msg     the message to output
334      *
335      * @return boolean success result
336      */
337
338     function showResult($success, $msg)
339     {
340         $this->startXML();
341         $this->elementStart('notifyResult',
342                             array('success' => ($success) ? 'true' : 'false',
343                                   'msg'     => $msg));
344         $this->endXML();
345     }
346
347 }
348