3 * StatusNet - the distributed open-source microblogging tool
4 * Copyright (C) 2010 StatusNet, Inc.
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.
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.
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/>.
20 define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
22 $shortoptions = 'i:n:f:';
23 $longoptions = array('id=', 'nickname=', 'file=');
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.
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)
34 END_OF_RESTOREUSER_HELP;
36 require_once INSTALLDIR.'/scripts/commandline.inc';
37 require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
39 function getActivityStreamDocument()
41 $filename = get_option_value('f', 'file');
43 if (empty($filename)) {
48 if (!file_exists($filename)) {
49 throw new Exception("No such file '$filename'.");
52 if (!is_file($filename)) {
53 throw new Exception("Not a regular file: '$filename'.");
56 if (!is_readable($filename)) {
57 throw new Exception("File '$filename' not readable.");
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);
63 $xml = file_get_contents($filename);
65 $dom = DOMDocument::loadXML($xml);
67 if ($dom->documentElement->namespaceURI != Activity::ATOM ||
68 $dom->documentElement->localName != 'feed') {
69 throw new Exception("'$filename' is not an Atom feed.");
75 function importActivityStream($user, $doc)
77 $feed = $doc->documentElement;
79 $subjectEl = ActivityUtils::child($feed, Activity::SUBJECT, Activity::SPEC);
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));
86 throw new Exception("Feed doesn't have an <activity:subject> element.");
90 // TRANS: Commandline script output.
91 printfv(_("No user specified; using backup user.")."\n");
92 $user = userFromSubject($subject);
95 $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
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);
100 for ($i = $entries->length - 1; $i >= 0; $i--) {
102 $entry = $entries->item($i);
104 $activity = new Activity($entry, $feed);
106 switch ($activity->verb) {
107 case ActivityVerb::FOLLOW:
108 subscribeProfile($user, $subject, $activity);
110 case ActivityVerb::JOIN:
111 joinGroup($user, $activity);
113 case ActivityVerb::POST:
114 postNote($user, $activity);
117 throw new Exception("Unknown verb: {$activity->verb}");
119 } catch (Exception $e) {
120 print $e->getMessage()."\n";
126 function subscribeProfile($user, $subject, $activity)
128 $profile = $user->getProfile();
130 if ($activity->objects[0]->id == $subject->id) {
132 $other = $activity->actor;
133 $otherUser = User::staticGet('uri', $other->id);
135 if (!empty($otherUser)) {
136 $otherProfile = $otherUser->getProfile();
138 throw new Exception("Can't force remote user to subscribe.");
140 // XXX: don't do this for untrusted input!
141 Subscription::start($otherProfile, $profile);
143 } else if (empty($activity->actor) || $activity->actor->id == $subject->id) {
145 $other = $activity->objects[0];
146 $otherUser = User::staticGet('uri', $other->id);
148 if (!empty($otherUser)) {
149 $otherProfile = $otherUser->getProfile();
151 $oprofile = Ostatus_profile::ensureActivityObjectProfile($other);
152 $otherProfile = $oprofile->localProfile();
155 Subscription::start($profile, $otherProfile);
157 throw new Exception("This activity seems unrelated to our user.");
161 function joinGroup($user, $activity)
163 // XXX: check that actor == subject
165 $uri = $activity->objects[0]->id;
167 $group = User_group::staticGet('uri', $uri);
170 $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]);
171 if (!$oprofile->isGroup()) {
172 throw new Exception("Remote profile is not a group!");
174 $group = $oprofile->localGroup();
177 assert(!empty($group));
179 if (Event::handle('StartJoinGroup', array($group, $user))) {
180 Group_member::join($group->id, $user->id);
181 Event::handle('EndJoinGroup', array($group, $user));
185 // XXX: largely cadged from Ostatus_profile::processNote()
187 function postNote($user, $activity)
189 $note = $activity->objects[0];
191 $sourceUri = $note->id;
193 $notice = Notice::staticGet('uri', $sourceUri);
195 if (!empty($notice)) {
197 $orig = clone($notice);
198 $notice->profile_id = $user->id;
199 $notice->update($orig);
203 // Use summary as fallback for content
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;
212 // @fixme fetch from $sourceUrl?
213 // @todo i18n FIXME: use sprintf and add i18n.
214 throw new ClientException("No content for notice {$sourceUri}.");
217 // Get (safe!) HTML and text versions of the content
219 $rendered = purify($sourceContent);
220 $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8');
222 $shortened = common_shorten_links($content);
224 $options = array('is_local' => Notice::LOCAL_PUBLIC,
226 'rendered' => $rendered,
227 'replies' => array(),
232 // Check for optional attributes...
234 if (!empty($activity->time)) {
235 $options['created'] = common_sql_date($activity->time);
238 if ($activity->context) {
239 // Any individual or group attn: targets?
241 list($options['groups'], $options['replies']) = filterAttention($activity->context->attention);
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);
249 $options['reply_to'] = $orig->id;
253 $location = $activity->context->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;
265 // Atom categories <-> hashtags
267 foreach ($activity->categories as $cat) {
269 $term = common_canonical_tag($cat->term);
271 $options['tags'][] = $term;
276 // Atom enclosures -> attachment URLs
277 foreach ($activity->enclosures as $href) {
278 // @fixme save these locally or....?
279 $options['urls'][] = $href;
282 $saved = Notice::saveNew($user->id,
284 'restore', // TODO: restore the actual source
290 function filterAttention($attn)
295 foreach (array_unique($attn) as $recipient) {
297 // Is the recipient a local user?
299 $user = User::staticGet('uri', $recipient);
302 // @fixme sender verification, spam etc?
303 $replies[] = $recipient;
307 // Is the recipient a remote group?
308 $oprofile = Ostatus_profile::ensureProfileURI($recipient);
311 if (!$oprofile->isGroup()) {
312 // may be canonicalized or something
313 $replies[] = $oprofile->uri;
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);
324 $group = User_group::staticGet('id', $id);
326 // Deliver to all members of this local group if allowed.
327 $profile = $sender->localProfile();
328 if ($profile->isMember($group)) {
329 $groups[] = $group->id;
331 common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member");
335 common_log(LOG_INFO, "Skipping reply to bogus group $recipient");
340 return array($groups, $replies);
343 function userFromSubject($subject)
345 $user = User::staticGet('uri', $subject->id);
349 array('nickname' => Ostatus_profile::getActivityObjectNickname($subject),
350 'uri' => $subject->id);
352 $user = User::register($attrs);
355 $profile = $user->getProfile();
356 Ostatus_profile::updateProfile($profile, $subject);
358 // FIXME: Update avatar
362 function purify($content)
364 $config = array('safe' => 1,
365 'deny_attribute' => 'id,style,on*');
366 return htmLawed($content, $config);
372 } catch (NoUserArgumentException $noae) {
375 $doc = getActivityStreamDocument();
376 importActivityStream($user, $doc);
377 } catch (Exception $e) {
378 print $e->getMessage()."\n";