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