]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/activityimporter.php
Merge branch '1.0.x' of git://gitorious.org/statusnet/mainline
[quix0rs-gnu-social.git] / lib / activityimporter.php
1 <?php
2 /**
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2010, StatusNet, Inc.
5  *
6  * class to import activities as part of a user's timeline
7  *
8  * PHP version 5
9  *
10  * This program is free software: you can redistribute it and/or modify
11  * it under the terms of the GNU Affero General Public License as published by
12  * the Free Software Foundation, either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU Affero General Public License for more details.
19  *
20  * You should have received a copy of the GNU Affero General Public License
21  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  *
23  * @category  Cache
24  * @package   StatusNet
25  * @author    Evan Prodromou <evan@status.net>
26  * @copyright 2010 StatusNet, Inc.
27  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
28  * @link      http://status.net/
29  */
30
31 if (!defined('STATUSNET')) {
32     // This check helps protect against security problems;
33     // your code file can't be executed directly from the web.
34     exit(1);
35 }
36
37 /**
38  * Class comment
39  *
40  * @category  General
41  * @package   StatusNet
42  * @author    Evan Prodromou <evan@status.net>
43  * @copyright 2010 StatusNet, Inc.
44  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
45  * @link      http://status.net/
46  */
47 class ActivityImporter extends QueueHandler
48 {
49     private $trusted = false;
50
51     /**
52      * Function comment
53      *
54      * @param
55      *
56      * @return
57      */
58     function handle($data)
59     {
60         list($user, $author, $activity, $trusted) = $data;
61
62         $this->trusted = $trusted;
63
64         $done = null;
65
66         if (Event::handle('StartImportActivity',
67                           array($user, $author, $activity, $trusted, &$done))) {
68             try {
69                 switch ($activity->verb) {
70                 case ActivityVerb::FOLLOW:
71                     $this->subscribeProfile($user, $author, $activity);
72                     break;
73                 case ActivityVerb::JOIN:
74                     $this->joinGroup($user, $activity);
75                     break;
76                 case ActivityVerb::POST:
77                     $this->postNote($user, $author, $activity);
78                     break;
79                 default:
80                     // TRANS: Client exception thrown when using an unknown verb for the activity importer.
81                     throw new ClientException(sprintf(_("Unknown verb: \"%s\"."),$activity->verb));
82                 }
83                 Event::handle('EndImportActivity',
84                               array($user, $author, $activity, $trusted));
85                 $done = true;
86             } catch (ClientException $ce) {
87                 common_log(LOG_WARNING, $ce->getMessage());
88                 $done = true;
89             } catch (ServerException $se) {
90                 common_log(LOG_ERR, $se->getMessage());
91                 $done = false;
92             } catch (Exception $e) {
93                 common_log(LOG_ERR, $e->getMessage());
94                 $done = false;
95             }
96         }
97         return $done;
98     }
99
100     function subscribeProfile($user, $author, $activity)
101     {
102         $profile = $user->getProfile();
103
104         if ($activity->objects[0]->id == $author->id) {
105             if (!$this->trusted) {
106                 // TRANS: Client exception thrown when trying to force a subscription for an untrusted user.
107                 throw new ClientException(_("Cannot force subscription for untrusted user."));
108             }
109
110             $other = $activity->actor;
111             $otherUser = User::staticGet('uri', $other->id);
112
113             if (!empty($otherUser)) {
114                 $otherProfile = $otherUser->getProfile();
115             } else {
116                 // TRANS: Client exception thrown when trying to for a remote user to subscribe.
117                 throw new Exception(_("Cannot force remote user to subscribe."));
118             }
119
120             // XXX: don't do this for untrusted input!
121
122             Subscription::start($otherProfile, $profile);
123         } else if (empty($activity->actor)
124                    || $activity->actor->id == $author->id) {
125
126             $other = $activity->objects[0];
127
128             $otherProfile = Profile::fromUri($other->id);
129
130             if (empty($otherProfile)) {
131                 // TRANS: Client exception thrown when trying to subscribe to an unknown profile.
132                 throw new ClientException(_("Unknown profile."));
133             }
134
135             Subscription::start($profile, $otherProfile);
136         } else {
137             // TRANS: Client exception thrown when trying to import an event not related to the importing user.
138             throw new Exception(_("This activity seems unrelated to our user."));
139         }
140     }
141
142     function joinGroup($user, $activity)
143     {
144         // XXX: check that actor == subject
145
146         $uri = $activity->objects[0]->id;
147
148         $group = User_group::staticGet('uri', $uri);
149
150         if (empty($group)) {
151             $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]);
152             if (!$oprofile->isGroup()) {
153                 // TRANS: Client exception thrown when trying to join a remote group that is not a group.
154                 throw new ClientException(_("Remote profile is not a group!"));
155             }
156             $group = $oprofile->localGroup();
157         }
158
159         assert(!empty($group));
160
161         if ($user->isMember($group)) {
162             // TRANS: Client exception thrown when trying to join a group the importing user is already a member of.
163             throw new ClientException(_("User is already a member of this group."));
164         }
165
166         if (Event::handle('StartJoinGroup', array($group, $user))) {
167             Group_member::join($group->id, $user->id);
168             Event::handle('EndJoinGroup', array($group, $user));
169         }
170     }
171
172     // XXX: largely cadged from Ostatus_profile::processNote()
173
174     function postNote($user, $author, $activity)
175     {
176         $note = $activity->objects[0];
177
178         $sourceUri = $note->id;
179
180         $notice = Notice::staticGet('uri', $sourceUri);
181
182         if (!empty($notice)) {
183
184             common_log(LOG_INFO, "Notice {$sourceUri} already exists.");
185
186             if ($this->trusted) {
187
188                 $profile = $notice->getProfile();
189
190                 $uri = $profile->getUri();
191
192                 if ($uri == $author->id) {
193                     common_log(LOG_INFO, "Updating notice author from $author->id to $user->uri");
194                     $orig = clone($notice);
195                     $notice->profile_id = $user->id;
196                     $notice->update($orig);
197                     return;
198                 } else {
199                     // TRANS: Client exception thrown when trying to import a notice by another user.
200                     // TRANS: %1$s is the source URI of the notice, %2$s is the URI of the author.
201                     throw new ClientException(sprintf(_('Already know about notice %1$s and '.
202                                                         ' it has a different author %2$s.'),
203                                                       $sourceUri, $uri));
204                 }
205             } else {
206                 // TRANS: Client exception thrown when trying to overwrite the author information for a non-trusted user during import.
207                 throw new ClientException(_("Not overwriting author info for non-trusted user."));
208             }
209         }
210
211         // Use summary as fallback for content
212
213         if (!empty($note->content)) {
214             $sourceContent = $note->content;
215         } else if (!empty($note->summary)) {
216             $sourceContent = $note->summary;
217         } else if (!empty($note->title)) {
218             $sourceContent = $note->title;
219         } else {
220             // @fixme fetch from $sourceUrl?
221             // TRANS: Client exception thrown when trying to import a notice without content.
222             // TRANS: %s is the notice URI.
223             throw new ClientException(sprintf(_("No content for notice %s."),$sourceUri));
224         }
225
226         // Get (safe!) HTML and text versions of the content
227
228         $rendered = $this->purify($sourceContent);
229         $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8');
230
231         $shortened = $user->shortenLinks($content);
232
233         $options = array('is_local' => Notice::LOCAL_PUBLIC,
234                          'uri' => $sourceUri,
235                          'rendered' => $rendered,
236                          'replies' => array(),
237                          'groups' => array(),
238                          'tags' => array(),
239                          'urls' => array(),
240                          'distribute' => false);
241
242         // Check for optional attributes...
243
244         if (!empty($activity->time)) {
245             $options['created'] = common_sql_date($activity->time);
246         }
247
248         if ($activity->context) {
249             // Any individual or group attn: targets?
250
251             list($options['groups'], $options['replies']) = $this->filterAttention($activity->context->attention);
252
253             // Maintain direct reply associations
254             // @fixme what about conversation ID?
255             if (!empty($activity->context->replyToID)) {
256                 $orig = Notice::staticGet('uri',
257                                           $activity->context->replyToID);
258                 if (!empty($orig)) {
259                     $options['reply_to'] = $orig->id;
260                 }
261             }
262
263             $location = $activity->context->location;
264
265             if ($location) {
266                 $options['lat'] = $location->lat;
267                 $options['lon'] = $location->lon;
268                 if ($location->location_id) {
269                     $options['location_ns'] = $location->location_ns;
270                     $options['location_id'] = $location->location_id;
271                 }
272             }
273         }
274
275         // Atom categories <-> hashtags
276
277         foreach ($activity->categories as $cat) {
278             if ($cat->term) {
279                 $term = common_canonical_tag($cat->term);
280                 if ($term) {
281                     $options['tags'][] = $term;
282                 }
283             }
284         }
285
286         // Atom enclosures -> attachment URLs
287         foreach ($activity->enclosures as $href) {
288             // @fixme save these locally or....?
289             $options['urls'][] = $href;
290         }
291
292         common_log(LOG_INFO, "Saving notice {$options['uri']}");
293
294         $saved = Notice::saveNew($user->id,
295                                  $content,
296                                  'restore', // TODO: restore the actual source
297                                  $options);
298
299         return $saved;
300     }
301
302     function filterAttention($attn)
303     {
304         $groups = array();
305         $replies = array();
306
307         foreach (array_unique($attn) as $recipient) {
308
309             // Is the recipient a local user?
310
311             $user = User::staticGet('uri', $recipient);
312
313             if ($user) {
314                 // @fixme sender verification, spam etc?
315                 $replies[] = $recipient;
316                 continue;
317             }
318
319             // Is the recipient a remote group?
320             $oprofile = Ostatus_profile::ensureProfileURI($recipient);
321
322             if ($oprofile) {
323                 if (!$oprofile->isGroup()) {
324                     // may be canonicalized or something
325                     $replies[] = $oprofile->uri;
326                 }
327                 continue;
328             }
329
330             // Is the recipient a local group?
331             // @fixme uri on user_group isn't reliable yet
332             // $group = User_group::staticGet('uri', $recipient);
333             $id = OStatusPlugin::localGroupFromUrl($recipient);
334
335             if ($id) {
336                 $group = User_group::staticGet('id', $id);
337                 if ($group) {
338                     // Deliver to all members of this local group if allowed.
339                     $profile = $sender->localProfile();
340                     if ($profile->isMember($group)) {
341                         $groups[] = $group->id;
342                     } else {
343                         common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member");
344                     }
345                     continue;
346                 } else {
347                     common_log(LOG_INFO, "Skipping reply to bogus group $recipient");
348                 }
349             }
350         }
351
352         return array($groups, $replies);
353     }
354
355
356     function purify($content)
357     {
358         require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
359
360         $config = array('safe' => 1,
361                         'deny_attribute' => 'id,style,on*');
362
363         return htmLawed($content, $config);
364     }
365 }