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