]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/feedimporter.php
Move accountrestorer class to feed importer
[quix0rs-gnu-social.git] / lib / feedimporter.php
1 <?php
2 /**
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2010, StatusNet, Inc.
5  *
6  * A class for restoring accounts
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  Account
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  * A class for restoring accounts
39  *
40  * This is a clumsy objectification of the functions in restoreuser.php.
41  * 
42  * Note that it quite illegally uses the OStatus_profile class which may
43  * not even exist on this server.
44  * 
45  * @category  Account
46  * @package   StatusNet
47  * @author    Evan Prodromou <evan@status.net>
48  * @copyright 2010 StatusNet, Inc.
49  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
50  * @link      http://status.net/
51  */
52
53 class AccountRestorer
54 {
55     private $_trusted = false;
56
57     function loadXML($xml)
58     {
59         $dom = DOMDocument::loadXML($xml);
60
61         if ($dom->documentElement->namespaceURI != Activity::ATOM ||
62             $dom->documentElement->localName != 'feed') {
63             throw new Exception("'$filename' is not an Atom feed.");
64         }
65
66         return $dom;
67     }
68
69     function importActivityStream($user, $doc)
70     {
71         $feed = $doc->documentElement;
72
73         $subjectEl = ActivityUtils::child($feed, Activity::SUBJECT, Activity::SPEC);
74
75         if (!empty($subjectEl)) {
76             $subject = new ActivityObject($subjectEl);
77         } else {
78             throw new Exception("Feed doesn't have an <activity:subject> element.");
79         }
80
81         if (is_null($user)) {
82             $user = $this->userFromSubject($subject);
83         }
84
85         $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
86
87         $activities = $this->entriesToActivities($entries, $feed);
88
89         // XXX: sort entries here
90
91         foreach ($activities as $activity) {
92             try {
93                 switch ($activity->verb) {
94                 case ActivityVerb::FOLLOW:
95                     $this->subscribeProfile($user, $subject, $activity);
96                     break;
97                 case ActivityVerb::JOIN:
98                     $this->joinGroup($user, $activity);
99                     break;
100                 case ActivityVerb::POST:
101                     $this->postNote($user, $activity);
102                     break;
103                 default:
104                     throw new Exception("Unknown verb: {$activity->verb}");
105                 }
106             } catch (Exception $e) {
107                 common_log(LOG_WARNING, $e->getMessage());
108                 continue;
109             }
110         }
111     }
112
113     function subscribeProfile($user, $subject, $activity)
114     {
115         $profile = $user->getProfile();
116
117         if ($activity->objects[0]->id == $subject->id) {
118             if (!$this->_trusted) {
119                     throw new Exception("Skipping a pushed subscription.");
120             } else {
121                 $other = $activity->actor;
122                 $otherUser = User::staticGet('uri', $other->id);
123
124                 if (!empty($otherUser)) {
125                     $otherProfile = $otherUser->getProfile();
126                 } else {
127                     throw new Exception("Can't force remote user to subscribe.");
128                 }
129                 // XXX: don't do this for untrusted input!
130                 Subscription::start($otherProfile, $profile);
131             }
132         } else if (empty($activity->actor) 
133                    || $activity->actor->id == $subject->id) {
134
135             $other = $activity->objects[0];
136             $otherUser = User::staticGet('uri', $other->id);
137
138             if (!empty($otherUser)) {
139                 $otherProfile = $otherUser->getProfile();
140             } else {
141                 $oprofile = Ostatus_profile::ensureActivityObjectProfile($other);
142                 $otherProfile = $oprofile->localProfile();
143             }
144
145             Subscription::start($profile, $otherProfile);
146         } else {
147             throw new Exception("This activity seems unrelated to our user.");
148         }
149     }
150
151     function joinGroup($user, $activity)
152     {
153         // XXX: check that actor == subject
154
155         $uri = $activity->objects[0]->id;
156
157         $group = User_group::staticGet('uri', $uri);
158
159         if (empty($group)) {
160             $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]);
161             if (!$oprofile->isGroup()) {
162                 throw new Exception("Remote profile is not a group!");
163             }
164             $group = $oprofile->localGroup();
165         }
166
167         assert(!empty($group));
168
169         if (Event::handle('StartJoinGroup', array($group, $user))) {
170             Group_member::join($group->id, $user->id);
171             Event::handle('EndJoinGroup', array($group, $user));
172         }
173     }
174
175     // XXX: largely cadged from Ostatus_profile::processNote()
176
177     function postNote($user, $activity)
178     {
179         $note = $activity->objects[0];
180
181         $sourceUri = $note->id;
182
183         $notice = Notice::staticGet('uri', $sourceUri);
184
185         if (!empty($notice)) {
186             // This is weird.
187             $orig = clone($notice);
188             $notice->profile_id = $user->id;
189             $notice->update($orig);
190             return;
191         }
192
193         // Use summary as fallback for content
194
195         if (!empty($note->content)) {
196             $sourceContent = $note->content;
197         } else if (!empty($note->summary)) {
198             $sourceContent = $note->summary;
199         } else if (!empty($note->title)) {
200             $sourceContent = $note->title;
201         } else {
202             // @fixme fetch from $sourceUrl?
203             // @todo i18n FIXME: use sprintf and add i18n.
204             throw new ClientException("No content for notice {$sourceUri}.");
205         }
206
207         // Get (safe!) HTML and text versions of the content
208
209         $rendered = $this->purify($sourceContent);
210         $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8');
211
212         $shortened = $user->shortenLinks($content);
213
214         $options = array('is_local' => Notice::LOCAL_PUBLIC,
215                          'uri' => $sourceUri,
216                          'rendered' => $rendered,
217                          'replies' => array(),
218                          'groups' => array(),
219                          'tags' => array(),
220                          'urls' => array());
221
222         // Check for optional attributes...
223
224         if (!empty($activity->time)) {
225             $options['created'] = common_sql_date($activity->time);
226         }
227
228         if ($activity->context) {
229             // Any individual or group attn: targets?
230
231             list($options['groups'], $options['replies']) = $this->filterAttention($activity->context->attention);
232
233             // Maintain direct reply associations
234             // @fixme what about conversation ID?
235             if (!empty($activity->context->replyToID)) {
236                 $orig = Notice::staticGet('uri',
237                                           $activity->context->replyToID);
238                 if (!empty($orig)) {
239                     $options['reply_to'] = $orig->id;
240                 }
241             }
242
243             $location = $activity->context->location;
244
245             if ($location) {
246                 $options['lat'] = $location->lat;
247                 $options['lon'] = $location->lon;
248                 if ($location->location_id) {
249                     $options['location_ns'] = $location->location_ns;
250                     $options['location_id'] = $location->location_id;
251                 }
252             }
253         }
254
255         // Atom categories <-> hashtags
256
257         foreach ($activity->categories as $cat) {
258             if ($cat->term) {
259                 $term = common_canonical_tag($cat->term);
260                 if ($term) {
261                     $options['tags'][] = $term;
262                 }
263             }
264         }
265
266         // Atom enclosures -> attachment URLs
267         foreach ($activity->enclosures as $href) {
268             // @fixme save these locally or....?
269             $options['urls'][] = $href;
270         }
271
272         $saved = Notice::saveNew($user->id,
273                                  $content,
274                                  'restore', // TODO: restore the actual source
275                                  $options);
276
277         return $saved;
278     }
279
280     function filterAttention($attn)
281     {
282         $groups = array();
283         $replies = array();
284
285         foreach (array_unique($attn) as $recipient) {
286
287             // Is the recipient a local user?
288
289             $user = User::staticGet('uri', $recipient);
290
291             if ($user) {
292                 // @fixme sender verification, spam etc?
293                 $replies[] = $recipient;
294                 continue;
295             }
296
297             // Is the recipient a remote group?
298             $oprofile = Ostatus_profile::ensureProfileURI($recipient);
299
300             if ($oprofile) {
301                 if (!$oprofile->isGroup()) {
302                     // may be canonicalized or something
303                     $replies[] = $oprofile->uri;
304                 }
305                 continue;
306             }
307
308             // Is the recipient a local group?
309             // @fixme uri on user_group isn't reliable yet
310             // $group = User_group::staticGet('uri', $recipient);
311             $id = OStatusPlugin::localGroupFromUrl($recipient);
312
313             if ($id) {
314                 $group = User_group::staticGet('id', $id);
315                 if ($group) {
316                     // Deliver to all members of this local group if allowed.
317                     $profile = $sender->localProfile();
318                     if ($profile->isMember($group)) {
319                         $groups[] = $group->id;
320                     } else {
321                         common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member");
322                     }
323                     continue;
324                 } else {
325                     common_log(LOG_INFO, "Skipping reply to bogus group $recipient");
326                 }
327             }
328         }
329
330         return array($groups, $replies);
331     }
332  
333    function userFromSubject($subject)
334     {
335         $user = User::staticGet('uri', $subject->id);
336
337         if (empty($user)) {
338             $attrs =
339                 array('nickname' => Ostatus_profile::getActivityObjectNickname($subject),
340                       'uri' => $subject->id);
341
342             $user = User::register($attrs);
343         }
344
345         $profile = $user->getProfile();
346         Ostatus_profile::updateProfile($profile, $subject);
347
348         // FIXME: Update avatar
349         return $user;
350     }
351
352     function purify($content)
353     {
354         $config = array('safe' => 1,
355                         'deny_attribute' => 'id,style,on*');
356         return htmLawed($content, $config);
357     }
358 }