]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/OStatus/classes/Ostatus_profile.php
748ecce18ff2cda8dbeacbbe66a93effe42e73c0
[quix0rs-gnu-social.git] / plugins / OStatus / classes / Ostatus_profile.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     $profile->subscribe()
29         generate random verification token
30             save to verify_token
31         sends a sub request to the hub...
32     
33     main/push/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     main/push/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 Ostatus_profile extends Memcached_DataObject
55 {
56     public $__table = 'ostatus_profile';
57
58     public $id;
59     public $profile_id;
60     public $group_id;
61
62     public $feeduri;
63     public $homeuri;
64
65     // PuSH subscription data
66     public $huburi;
67     public $secret;
68     public $verify_token;
69     public $sub_state; // subscribe, active, unsubscribe
70     public $sub_start;
71     public $sub_end;
72
73     public $salmonuri;
74
75     public $created;
76     public $lastupdate;
77
78
79     public /*static*/ function staticGet($k, $v=null)
80     {
81         return parent::staticGet(__CLASS__, $k, $v);
82     }
83
84     /**
85      * return table definition for DB_DataObject
86      *
87      * DB_DataObject needs to know something about the table to manipulate
88      * instances. This method provides all the DB_DataObject needs to know.
89      *
90      * @return array array of column definitions
91      */
92
93     function table()
94     {
95         return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
96                      'profile_id' => DB_DATAOBJECT_INT,
97                      'group_id' => DB_DATAOBJECT_INT,
98                      'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
99                      'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
100                      'huburi' =>  DB_DATAOBJECT_STR,
101                      'secret' => DB_DATAOBJECT_STR,
102                      'verify_token' => DB_DATAOBJECT_STR,
103                      'sub_state' => DB_DATAOBJECT_STR,
104                      'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
105                      'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
106                      'salmonuri' =>  DB_DATAOBJECT_STR,
107                      'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
108                      'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
109     }
110     
111     static function schemaDef()
112     {
113         return array(new ColumnDef('id', 'integer',
114                                    /*size*/ null,
115                                    /*nullable*/ false,
116                                    /*key*/ 'PRI',
117                                    /*default*/ '0',
118                                    /*extra*/ null,
119                                    /*auto_increment*/ true),
120                      new ColumnDef('profile_id', 'integer',
121                                    null, true, 'UNI'),
122                      new ColumnDef('group_id', 'integer',
123                                    null, true, 'UNI'),
124                      new ColumnDef('feeduri', 'varchar',
125                                    255, false, 'UNI'),
126                      new ColumnDef('homeuri', 'varchar',
127                                    255, false),
128                      new ColumnDef('huburi', 'text',
129                                    null, true),
130                      new ColumnDef('verify_token', 'varchar',
131                                    32, true),
132                      new ColumnDef('secret', 'varchar',
133                                    64, true),
134                      new ColumnDef('sub_state', "enum('subscribe','active','unsubscribe')",
135                                    null, true),
136                      new ColumnDef('sub_start', 'datetime',
137                                    null, true),
138                      new ColumnDef('sub_end', 'datetime',
139                                    null, true),
140                      new ColumnDef('salmonuri', 'text',
141                                    null, true),
142                      new ColumnDef('created', 'datetime',
143                                    null, false),
144                      new ColumnDef('lastupdate', 'datetime',
145                                    null, false));
146     }
147
148     /**
149      * return key definitions for DB_DataObject
150      *
151      * DB_DataObject needs to know about keys that the table has; this function
152      * defines them.
153      *
154      * @return array key definitions
155      */
156
157     function keys()
158     {
159         return array_keys($this->keyTypes());
160     }
161
162     /**
163      * return key definitions for Memcached_DataObject
164      *
165      * Our caching system uses the same key definitions, but uses a different
166      * method to get them.
167      *
168      * @return array key definitions
169      */
170
171     function keyTypes()
172     {
173         return array('id' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U');
174     }
175
176     function sequenceKey()
177     {
178         return array('id', true, false);
179     }
180
181     /**
182      * Fetch the StatusNet-side profile for this feed
183      * @return Profile
184      */
185     public function getLocalProfile()
186     {
187         return Profile::staticGet('id', $this->profile_id);
188     }
189
190     /**
191      * @param FeedMunger $munger
192      * @param boolean $isGroup is this a group record?
193      * @return Ostatus_profile
194      */
195     public static function ensureProfile($munger)
196     {
197         $entity = $munger->ostatusProfile();
198
199         $current = self::staticGet('feeduri', $entity->feeduri);
200         if ($current) {
201             // @fixme we should probably update info as necessary
202             return $current;
203         }
204
205         $entity->query('BEGIN');
206
207         // Awful hack! Awful hack!
208         $entity->verify = common_good_rand(16);
209         $entity->secret = common_good_rand(32);
210
211         try {
212             $profile = $munger->profile();
213             $result = $profile->insert();
214             if (empty($result)) {
215                 throw new FeedDBException($profile);
216             }
217
218             $avatar = $munger->getAvatar();
219             if ($avatar) {
220                 // @fixme this should be better encapsulated
221                 // ripped from oauthstore.php (for old OMB client)
222                 $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
223                 copy($avatar, $temp_filename);
224                 $imagefile = new ImageFile($profile->id, $temp_filename);
225                 $filename = Avatar::filename($profile->id,
226                                              image_type_to_extension($imagefile->type),
227                                              null,
228                                              common_timestamp());
229                 rename($temp_filename, Avatar::path($filename));
230                 $profile->setOriginal($filename);
231             }
232
233             $entity->profile_id = $profile->id;
234             if ($entity->isGroup()) {
235                 $group = new User_group();
236                 $group->nickname = $profile->nickname . '@remote'; // @fixme
237                 $group->fullname = $profile->fullname;
238                 $group->homepage = $profile->homepage;
239                 $group->location = $profile->location;
240                 $group->created = $profile->created;
241                 $group->insert();
242
243                 if ($avatar) {
244                     $group->setOriginal($filename);
245                 }
246
247                 $entity->group_id = $group->id;
248             }
249
250             $result = $entity->insert();
251             if (empty($result)) {
252                 throw new FeedDBException($entity);
253             }
254
255             $entity->query('COMMIT');
256         } catch (FeedDBException $e) {
257             common_log_db_error($e->obj, 'INSERT', __FILE__);
258             $entity->query('ROLLBACK');
259             return false;
260         }
261         return $entity;
262     }
263
264     /**
265      * Damn dirty hack!
266      */
267     function isGroup()
268     {
269         return (strpos($this->feeduri, '/groups/') !== false);
270     }
271
272     /**
273      * Send a subscription request to the hub for this feed.
274      * The hub will later send us a confirmation POST to /main/push/callback.
275      *
276      * @return bool true on success, false on failure
277      */
278     public function subscribe($mode='subscribe')
279     {
280         if (common_config('feedsub', 'nohub')) {
281             // Fake it! We're just testing remote feeds w/o hubs.
282             return true;
283         }
284         // @fixme use the verification token
285         #$token = md5(mt_rand() . ':' . $this->feeduri);
286         #$this->verify_token = $token;
287         #$this->update(); // @fixme
288         try {
289             $callback = common_local_url('pushcallback', array('feed' => $this->id));
290             $headers = array('Content-Type: application/x-www-form-urlencoded');
291             $post = array('hub.mode' => $mode,
292                           'hub.callback' => $callback,
293                           'hub.verify' => 'async',
294                           'hub.verify_token' => $this->verify_token,
295                           'hub.secret' => $this->secret,
296                           //'hub.lease_seconds' => 0,
297                           'hub.topic' => $this->feeduri);
298             $client = new HTTPClient();
299             $response = $client->post($this->huburi, $headers, $post);
300             $status = $response->getStatus();
301             if ($status == 202) {
302                 common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
303                 return true;
304             } else if ($status == 204) {
305                 common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified');
306                 return true;
307             } else if ($status >= 200 && $status < 300) {
308                 common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
309                 return false;
310             } else {
311                 common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody());
312                 return false;
313             }
314         } catch (Exception $e) {
315             // wtf!
316             common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri");
317             return false;
318         }
319     }
320
321     /**
322      * Send an unsubscription request to the hub for this feed.
323      * The hub will later send us a confirmation POST to /main/push/callback.
324      *
325      * @return bool true on success, false on failure
326      */
327     public function unsubscribe() {
328         return $this->subscribe('unsubscribe');
329     }
330
331     /**
332      * Read and post notices for updates from the feed.
333      * Currently assumes that all items in the feed are new,
334      * coming from a PuSH hub.
335      *
336      * @param string $xml source of Atom or RSS feed
337      * @param string $hmac X-Hub-Signature header, if present
338      */
339     public function postUpdates($xml, $hmac)
340     {
341         common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $hmac $xml");
342
343         if ($this->secret) {
344             if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) {
345                 $their_hmac = strtolower($matches[1]);
346                 $our_hmac = hash_hmac('sha1', $xml, $this->secret);
347                 if ($their_hmac !== $our_hmac) {
348                     common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac");
349                     return;
350                 }
351             } else {
352                 common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
353                 return;
354             }
355         } else if ($hmac) {
356             common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'");
357             return;
358         }
359
360         require_once "XML/Feed/Parser.php";
361         $feed = new XML_Feed_Parser($xml, false, false, true);
362         $munger = new FeedMunger($feed);
363         
364         $hits = 0;
365         foreach ($feed as $index => $entry) {
366             // @fixme this might sort in wrong order if we get multiple updates
367
368             $notice = $munger->notice($index);
369
370             // Double-check for oldies
371             // @fixme this could explode horribly for multiple feeds on a blog. sigh
372             $dupe = new Notice();
373             $dupe->uri = $notice->uri;
374             if ($dupe->find(true)) {
375                 common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}");
376                 continue;
377             }
378
379             // @fixme need to ensure that groups get handled correctly
380             $saved = Notice::saveNew($notice->profile_id,
381                                      $notice->content,
382                                      'ostatus',
383                                      array('is_local' => Notice::REMOTE_OMB,
384                                            'uri' => $notice->uri,
385                                            'lat' => $notice->lat,
386                                            'lon' => $notice->lon,
387                                            'location_ns' => $notice->location_ns,
388                                            'location_id' => $notice->location_id));
389
390             /*
391             common_log(LOG_DEBUG, "going to check group delivery...");
392             if ($this->group_id) {
393                 $group = User_group::staticGet($this->group_id);
394                 if ($group) {
395                     common_log(LOG_INFO, __METHOD__ . ": saving to local shadow group $group->id $group->nickname");
396                     $groups = array($group);
397                 } else {
398                     common_log(LOG_INFO, __METHOD__ . ": lost the local shadow group?");
399                 }
400             } else {
401                 common_log(LOG_INFO, __METHOD__ . ": no local shadow groups");
402                 $groups = array();
403             }
404             common_log(LOG_DEBUG, "going to add to inboxes...");
405             $notice->addToInboxes($groups, array());
406             common_log(LOG_DEBUG, "added to inboxes.");
407             */
408
409             $hits++;
410         }
411         if ($hits == 0) {
412             common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml");
413         }
414     }
415 }