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