]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - scripts/restoreuser.php
Merge branch 'master' of gitorious.org:statusnet/mainline into 0.9.x
[quix0rs-gnu-social.git] / scripts / restoreuser.php
1 <?php
2 /*
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2010 StatusNet, Inc.
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU Affero General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU Affero General Public License for more details.
15  *
16  * You should have received a copy of the GNU Affero General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
21
22 $shortoptions = 'i:n:f:';
23 $longoptions = array('id=', 'nickname=', 'file=');
24
25 $helptext = <<<END_OF_RESTOREUSER_HELP
26 restoreuser.php [options]
27 Restore a backed-up user file to the database. If
28 neither ID or name provided, will create a new user.
29
30   -i --id       ID of user to export
31   -n --nickname nickname of the user to export
32   -f --file     file to read from (STDIN by default)
33
34 END_OF_RESTOREUSER_HELP;
35
36 require_once INSTALLDIR.'/scripts/commandline.inc';
37 require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
38
39 function getActivityStreamDocument()
40 {
41     $filename = get_option_value('f', 'file');
42
43     if (empty($filename)) {
44         show_help();
45         exit(1);
46     }
47
48     if (!file_exists($filename)) {
49         throw new Exception("No such file '$filename'.");
50     }
51
52     if (!is_file($filename)) {
53         throw new Exception("Not a regular file: '$filename'.");
54     }
55
56     if (!is_readable($filename)) {
57         throw new Exception("File '$filename' not readable.");
58     }
59
60     // TRANS: Commandline script output. %s is the filename that contains a backup for a user.
61     printfv(_("Getting backup from file '%s'.")."\n",$filename);
62
63     $xml = file_get_contents($filename);
64
65     $dom = DOMDocument::loadXML($xml);
66
67     if ($dom->documentElement->namespaceURI != Activity::ATOM ||
68         $dom->documentElement->localName != 'feed') {
69         throw new Exception("'$filename' is not an Atom feed.");
70     }
71
72     return $dom;
73 }
74
75 function importActivityStream($user, $doc)
76 {
77     $feed = $doc->documentElement;
78
79     $subjectEl = ActivityUtils::child($feed, Activity::SUBJECT, Activity::SPEC);
80
81     if (!empty($subjectEl)) {
82         $subject = new ActivityObject($subjectEl);
83         // TRANS: Commandline script output. %1$s is the subject ID, %2$s is the subject nickname.
84         printfv(_("Backup file for user %1$s (%2$s)")."\n", $subject->id, Ostatus_profile::getActivityObjectNickname($subject));
85     } else {
86         throw new Exception("Feed doesn't have an <activity:subject> element.");
87     }
88
89     if (is_null($user)) {
90         // TRANS: Commandline script output.
91         printfv(_("No user specified; using backup user.")."\n");
92         $user = userFromSubject($subject);
93     }
94
95     $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
96
97     // TRANS: Commandline script output. %d is the number of entries in the activity stream in backup; used for plural.
98     printfv(_m("%d entry in backup.","%d entries in backup.",$entries->length)."\n", $entries->length);
99
100     for ($i = $entries->length - 1; $i >= 0; $i--) {
101         try {
102             $entry = $entries->item($i);
103
104             $activity = new Activity($entry, $feed);
105
106             switch ($activity->verb) {
107             case ActivityVerb::FOLLOW:
108                 subscribeProfile($user, $subject, $activity);
109                 break;
110             case ActivityVerb::JOIN:
111                 joinGroup($user, $activity);
112                 break;
113             case ActivityVerb::POST:
114                 postNote($user, $activity);
115                 break;
116             default:
117                 throw new Exception("Unknown verb: {$activity->verb}");
118             }
119         } catch (Exception $e) {
120             print $e->getMessage()."\n";
121             continue;
122         }
123     }
124 }
125
126 function subscribeProfile($user, $subject, $activity)
127 {
128     $profile = $user->getProfile();
129
130     if ($activity->objects[0]->id == $subject->id) {
131
132         $other = $activity->actor;
133         $otherUser = User::staticGet('uri', $other->id);
134
135         if (!empty($otherUser)) {
136             $otherProfile = $otherUser->getProfile();
137         } else {
138             throw new Exception("Can't force remote user to subscribe.");
139         }
140         // XXX: don't do this for untrusted input!
141         Subscription::start($otherProfile, $profile);
142
143     } else if (empty($activity->actor) || $activity->actor->id == $subject->id) {
144
145         $other = $activity->objects[0];
146         $otherUser = User::staticGet('uri', $other->id);
147
148         if (!empty($otherUser)) {
149             $otherProfile = $otherUser->getProfile();
150         } else {
151             $oprofile = Ostatus_profile::ensureActivityObjectProfile($other);
152             $otherProfile = $oprofile->localProfile();
153         }
154
155         Subscription::start($profile, $otherProfile);
156     } else {
157         throw new Exception("This activity seems unrelated to our user.");
158     }
159 }
160
161 function joinGroup($user, $activity)
162 {
163     // XXX: check that actor == subject
164
165     $uri = $activity->objects[0]->id;
166
167     $group = User_group::staticGet('uri', $uri);
168
169     if (empty($group)) {
170         $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]);
171         if (!$oprofile->isGroup()) {
172             throw new Exception("Remote profile is not a group!");
173         }
174         $group = $oprofile->localGroup();
175     }
176
177     assert(!empty($group));
178
179     if (Event::handle('StartJoinGroup', array($group, $user))) {
180         Group_member::join($group->id, $user->id);
181         Event::handle('EndJoinGroup', array($group, $user));
182     }
183 }
184
185 // XXX: largely cadged from Ostatus_profile::processNote()
186
187 function postNote($user, $activity)
188 {
189     $note = $activity->objects[0];
190
191     $sourceUri = $note->id;
192
193     $notice = Notice::staticGet('uri', $sourceUri);
194
195     if (!empty($notice)) {
196         // This is weird.
197         $orig = clone($notice);
198         $notice->profile_id = $user->id;
199         $notice->update($orig);
200         return;
201     }
202
203     // Use summary as fallback for content
204
205     if (!empty($note->content)) {
206         $sourceContent = $note->content;
207     } else if (!empty($note->summary)) {
208         $sourceContent = $note->summary;
209     } else if (!empty($note->title)) {
210         $sourceContent = $note->title;
211     } else {
212         // @fixme fetch from $sourceUrl?
213         // @todo i18n FIXME: use sprintf and add i18n.
214         throw new ClientException("No content for notice {$sourceUri}.");
215     }
216
217     // Get (safe!) HTML and text versions of the content
218
219     $rendered = purify($sourceContent);
220     $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8');
221
222     $shortened = common_shorten_links($content);
223
224     $options = array('is_local' => Notice::LOCAL_PUBLIC,
225                      'uri' => $sourceUri,
226                      'rendered' => $rendered,
227                      'replies' => array(),
228                      'groups' => array(),
229                      'tags' => array(),
230                      'urls' => array());
231
232     // Check for optional attributes...
233
234     if (!empty($activity->time)) {
235         $options['created'] = common_sql_date($activity->time);
236     }
237
238     if ($activity->context) {
239         // Any individual or group attn: targets?
240
241         list($options['groups'], $options['replies']) = filterAttention($activity->context->attention);
242
243         // Maintain direct reply associations
244         // @fixme what about conversation ID?
245         if (!empty($activity->context->replyToID)) {
246             $orig = Notice::staticGet('uri',
247                                       $activity->context->replyToID);
248             if (!empty($orig)) {
249                 $options['reply_to'] = $orig->id;
250             }
251         }
252
253         $location = $activity->context->location;
254
255         if ($location) {
256             $options['lat'] = $location->lat;
257             $options['lon'] = $location->lon;
258             if ($location->location_id) {
259                 $options['location_ns'] = $location->location_ns;
260                 $options['location_id'] = $location->location_id;
261             }
262         }
263     }
264
265     // Atom categories <-> hashtags
266
267     foreach ($activity->categories as $cat) {
268         if ($cat->term) {
269             $term = common_canonical_tag($cat->term);
270             if ($term) {
271                 $options['tags'][] = $term;
272             }
273         }
274     }
275
276     // Atom enclosures -> attachment URLs
277     foreach ($activity->enclosures as $href) {
278         // @fixme save these locally or....?
279         $options['urls'][] = $href;
280     }
281
282     $saved = Notice::saveNew($user->id,
283                              $content,
284                              'restore', // TODO: restore the actual source
285                              $options);
286
287     return $saved;
288 }
289
290 function filterAttention($attn)
291 {
292     $groups = array();
293     $replies = array();
294
295     foreach (array_unique($attn) as $recipient) {
296
297         // Is the recipient a local user?
298
299         $user = User::staticGet('uri', $recipient);
300
301         if ($user) {
302             // @fixme sender verification, spam etc?
303             $replies[] = $recipient;
304             continue;
305         }
306
307         // Is the recipient a remote group?
308         $oprofile = Ostatus_profile::ensureProfileURI($recipient);
309
310         if ($oprofile) {
311             if (!$oprofile->isGroup()) {
312                 // may be canonicalized or something
313                 $replies[] = $oprofile->uri;
314             }
315             continue;
316         }
317
318         // Is the recipient a local group?
319         // @fixme uri on user_group isn't reliable yet
320         // $group = User_group::staticGet('uri', $recipient);
321         $id = OStatusPlugin::localGroupFromUrl($recipient);
322
323         if ($id) {
324             $group = User_group::staticGet('id', $id);
325             if ($group) {
326                 // Deliver to all members of this local group if allowed.
327                 $profile = $sender->localProfile();
328                 if ($profile->isMember($group)) {
329                     $groups[] = $group->id;
330                 } else {
331                     common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member");
332                 }
333                 continue;
334             } else {
335                 common_log(LOG_INFO, "Skipping reply to bogus group $recipient");
336             }
337         }
338     }
339
340     return array($groups, $replies);
341 }
342
343 function userFromSubject($subject)
344 {
345     $user = User::staticGet('uri', $subject->id);
346
347     if (empty($user)) {
348         $attrs =
349           array('nickname' => Ostatus_profile::getActivityObjectNickname($subject),
350                 'uri' => $subject->id);
351
352         $user = User::register($attrs);
353     }
354
355     $profile = $user->getProfile();
356     Ostatus_profile::updateProfile($profile, $subject);
357
358     // FIXME: Update avatar
359     return $user;
360 }
361
362 function purify($content)
363 {
364     $config = array('safe' => 1,
365                     'deny_attribute' => 'id,style,on*');
366     return htmLawed($content, $config);
367 }
368
369 try {
370     try {
371         $user = getUser();
372     } catch (NoUserArgumentException $noae) {
373         $user = null;
374     }
375     $doc  = getActivityStreamDocument();
376     importActivityStream($user, $doc);
377 } catch (Exception $e) {
378     print $e->getMessage()."\n";
379     exit(1);
380 }