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