]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/OStatus/classes/Feedinfo.php
OStatus cleanup...
[quix0rs-gnu-social.git] / plugins / OStatus / classes / Feedinfo.php
1 <?php
2 /*
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2009-2010, StatusNet, Inc.
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU Affero General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU Affero General Public License for more details.
15  *
16  * You should have received a copy of the GNU Affero General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 /**
21  * @package FeedSubPlugin
22  * @maintainer Brion Vibber <brion@status.net>
23  */
24
25 /*
26 PuSH subscription flow:
27
28     $feedinfo->subscribe()
29         generate random verification token
30             save to verify_token
31         sends a sub request to the hub...
32     
33     feedsub/callback
34         hub sends confirmation back to us via GET
35         We verify the request, then echo back the challenge.
36         On our end, we save the time we subscribed and the lease expiration
37     
38     feedsub/callback
39         hub sends us updates via POST
40     
41 */
42
43 class FeedDBException extends FeedSubException
44 {
45     public $obj;
46
47     function __construct($obj)
48     {
49         parent::__construct('Database insert failure');
50         $this->obj = $obj;
51     }
52 }
53
54 class Feedinfo extends Memcached_DataObject
55 {
56     public $__table = 'feedinfo';
57
58     public $id;
59     public $profile_id;
60
61     public $feeduri;
62     public $homeuri;
63     public $huburi;
64
65     // PuSH subscription data
66     public $secret;
67     public $verify_token;
68     public $sub_start;
69     public $sub_end;
70
71     public $created;
72     public $lastupdate;
73
74
75     public /*static*/ function staticGet($k, $v=null)
76     {
77         return parent::staticGet(__CLASS__, $k, $v);
78     }
79
80     /**
81      * return table definition for DB_DataObject
82      *
83      * DB_DataObject needs to know something about the table to manipulate
84      * instances. This method provides all the DB_DataObject needs to know.
85      *
86      * @return array array of column definitions
87      */
88
89     function table()
90     {
91         return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
92                      'profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
93                      'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
94                      'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
95                      'huburi' =>  DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
96                      'secret' => DB_DATAOBJECT_STR,
97                      'verify_token' => DB_DATAOBJECT_STR,
98                      'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
99                      'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
100                      'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
101                      'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
102     }
103     
104     static function schemaDef()
105     {
106         return array(new ColumnDef('id', 'integer',
107                                    /*size*/ null,
108                                    /*nullable*/ false,
109                                    /*key*/ 'PRI',
110                                    /*default*/ '0',
111                                    /*extra*/ null,
112                                    /*auto_increment*/ true),
113                      new ColumnDef('profile_id', 'integer',
114                                    null, false),
115                      new ColumnDef('feeduri', 'varchar',
116                                    255, false, 'UNI'),
117                      new ColumnDef('homeuri', 'varchar',
118                                    255, false),
119                      new ColumnDef('huburi', 'varchar',
120                                    255, false),
121                      new ColumnDef('verify_token', 'varchar',
122                                    32, true),
123                      new ColumnDef('secret', 'varchar',
124                                    64, true),
125                      new ColumnDef('sub_start', 'datetime',
126                                    null, true),
127                      new ColumnDef('sub_end', 'datetime',
128                                    null, true),
129                      new ColumnDef('created', 'datetime',
130                                    null, false),
131                      new ColumnDef('lastupdate', 'datetime',
132                                    null, false));
133     }
134
135     /**
136      * return key definitions for DB_DataObject
137      *
138      * DB_DataObject needs to know about keys that the table has; this function
139      * defines them.
140      *
141      * @return array key definitions
142      */
143
144     function keys()
145     {
146         return array_keys($this->keyTypes());
147     }
148
149     /**
150      * return key definitions for Memcached_DataObject
151      *
152      * Our caching system uses the same key definitions, but uses a different
153      * method to get them.
154      *
155      * @return array key definitions
156      */
157
158     function keyTypes()
159     {
160         return array('id' => 'K'); // @fixme we'll need a profile_id key at least
161     }
162
163     function sequenceKey()
164     {
165         return array('id', true, false);
166     }
167
168     /**
169      * Fetch the StatusNet-side profile for this feed
170      * @return Profile
171      */
172     public function getProfile()
173     {
174         return Profile::staticGet('id', $this->profile_id);
175     }
176
177     /**
178      * @param FeedMunger $munger
179      * @return Feedinfo
180      */
181     public static function ensureProfile($munger)
182     {
183         $feedinfo = $munger->feedinfo();
184
185         $current = self::staticGet('feeduri', $feedinfo->feeduri);
186         if ($current) {
187             // @fixme we should probably update info as necessary
188             return $current;
189         }
190
191         $feedinfo->query('BEGIN');
192
193         // Awful hack! Awful hack!
194         $feedinfo->verify = common_good_rand(16);
195         $feedinfo->secret = common_good_rand(32);
196
197         try {
198             $profile = $munger->profile();
199             $result = $profile->insert();
200             if (empty($result)) {
201                 throw new FeedDBException($profile);
202             }
203
204             $avatar = $munger->getAvatar();
205             if ($avatar) {
206                 // @fixme this should be better encapsulated
207                 // ripped from oauthstore.php (for old OMB client)
208                 $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
209                 copy($avatar, $temp_filename);
210                 $imagefile = new ImageFile($profile->id, $temp_filename);
211                 $filename = Avatar::filename($profile->id,
212                                              image_type_to_extension($imagefile->type),
213                                              null,
214                                              common_timestamp());
215                 rename($temp_filename, Avatar::path($filename));
216                 $profile->setOriginal($filename);
217             }
218
219             $feedinfo->profile_id = $profile->id;
220             $result = $feedinfo->insert();
221             if (empty($result)) {
222                 throw new FeedDBException($feedinfo);
223             }
224
225             $feedinfo->query('COMMIT');
226         } catch (FeedDBException $e) {
227             common_log_db_error($e->obj, 'INSERT', __FILE__);
228             $feedinfo->query('ROLLBACK');
229             return false;
230         }
231         return $feedinfo;
232     }
233
234     /**
235      * Send a subscription request to the hub for this feed.
236      * The hub will later send us a confirmation POST to /feedsub/callback.
237      *
238      * @return bool true on success, false on failure
239      */
240     public function subscribe()
241     {
242         if (common_config('feedsub', 'nohub')) {
243             // Fake it! We're just testing remote feeds w/o hubs.
244             return true;
245         }
246         // @fixme use the verification token
247         #$token = md5(mt_rand() . ':' . $this->feeduri);
248         #$this->verify_token = $token;
249         #$this->update(); // @fixme
250         try {
251             $callback = common_local_url('pushcallback', array('feed' => $this->id));
252             $headers = array('Content-Type: application/x-www-form-urlencoded');
253             $post = array('hub.mode' => 'subscribe',
254                           'hub.callback' => $callback,
255                           'hub.verify' => 'async',
256                           'hub.verify_token' => $this->verify_token,
257                           'hub.secret' => $this->secret,
258                           //'hub.lease_seconds' => 0,
259                           'hub.topic' => $this->feeduri);
260             $client = new HTTPClient();
261             $response = $client->post($this->huburi, $headers, $post);
262             $status = $response->getStatus();
263             if ($status == 202) {
264                 common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
265                 return true;
266             } else if ($status == 204) {
267                 common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified');
268                 return true;
269             } else if ($status >= 200 && $status < 300) {
270                 common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
271                 return false;
272             } else {
273                 common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody());
274                 return false;
275             }
276         } catch (Exception $e) {
277             // wtf!
278             common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri");
279             return false;
280         }
281     }
282
283     /**
284      * Read and post notices for updates from the feed.
285      * Currently assumes that all items in the feed are new,
286      * coming from a PuSH hub.
287      *
288      * @param string $xml source of Atom or RSS feed
289      * @param string $hmac X-Hub-Signature header, if present
290      */
291     public function postUpdates($xml, $hmac)
292     {
293         common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml");
294
295         if ($this->secret) {
296             if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) {
297                 $their_hmac = strtolower($matches[1]);
298                 $our_hmac = sha1($xml . $this->secret);
299                 if ($their_hmac !== $our_hmac) {
300                     common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac");
301                     return;
302                 }
303             } else {
304                 common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
305                 return;
306             }
307         } else if ($hmac) {
308             common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'");
309             return;
310         }
311
312         require_once "XML/Feed/Parser.php";
313         $feed = new XML_Feed_Parser($xml, false, false, true);
314         $munger = new FeedMunger($feed);
315         
316         $hits = 0;
317         foreach ($feed as $index => $entry) {
318             // @fixme this might sort in wrong order if we get multiple updates
319             
320             $notice = $munger->notice($index);
321             $notice->profile_id = $this->profile_id;
322             
323             // Double-check for oldies
324             // @fixme this could explode horribly for multiple feeds on a blog. sigh
325             $dupe = new Notice();
326             $dupe->uri = $notice->uri;
327             if ($dupe->find(true)) {
328                 common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}");
329                 continue;
330             }
331             
332             if (Event::handle('StartNoticeSave', array(&$notice))) {
333                 $id = $notice->insert();
334                 Event::handle('EndNoticeSave', array($notice));
335             }
336             $notice->addToInboxes();
337
338             common_log(LOG_INFO, __METHOD__ . ": saved notice {$notice->id} for entry $index of update to \"{$this->feeduri}\"");
339             $hits++;
340         }
341         if ($hits == 0) {
342             common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml");
343         }
344     }
345 }