3 * StatusNet - the distributed open-source microblogging tool
4 * Copyright (C) 2010, StatusNet, Inc.
6 * A class for restoring accounts
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.
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.
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/>.
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/
31 if (!defined('STATUSNET')) {
32 // This check helps protect against security problems;
33 // your code file can't be executed directly from the web.
38 * A class for restoring accounts
40 * This is a clumsy objectification of the functions in restoreuser.php.
42 * Note that it quite illegally uses the OStatus_profile class which may
43 * not even exist on this server.
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/
55 function loadXML($xml)
57 $dom = DOMDocument::loadXML($xml);
59 if ($dom->documentElement->namespaceURI != Activity::ATOM ||
60 $dom->documentElement->localName != 'feed') {
61 throw new Exception("'$filename' is not an Atom feed.");
67 function importActivityStream($user, $doc)
69 $feed = $doc->documentElement;
71 $subjectEl = ActivityUtils::child($feed, Activity::SUBJECT, Activity::SPEC);
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));
78 throw new Exception("Feed doesn't have an <activity:subject> element.");
82 // TRANS: Commandline script output.
83 printfv(_("No user specified; using backup user.")."\n");
84 $user = $this->userFromSubject($subject);
87 $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
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);
92 for ($i = $entries->length - 1; $i >= 0; $i--) {
94 $entry = $entries->item($i);
96 $activity = new Activity($entry, $feed);
98 switch ($activity->verb) {
99 case ActivityVerb::FOLLOW:
100 $this->subscribeProfile($user, $subject, $activity);
102 case ActivityVerb::JOIN:
103 $this->joinGroup($user, $activity);
105 case ActivityVerb::POST:
106 $this->postNote($user, $activity);
109 throw new Exception("Unknown verb: {$activity->verb}");
111 } catch (Exception $e) {
112 print $e->getMessage()."\n";
118 function subscribeProfile($user, $subject, $activity)
120 $profile = $user->getProfile();
122 if ($activity->objects[0]->id == $subject->id) {
124 $other = $activity->actor;
125 $otherUser = User::staticGet('uri', $other->id);
127 if (!empty($otherUser)) {
128 $otherProfile = $otherUser->getProfile();
130 throw new Exception("Can't force remote user to subscribe.");
132 // XXX: don't do this for untrusted input!
133 Subscription::start($otherProfile, $profile);
135 } else if (empty($activity->actor) || $activity->actor->id == $subject->id) {
137 $other = $activity->objects[0];
138 $otherUser = User::staticGet('uri', $other->id);
140 if (!empty($otherUser)) {
141 $otherProfile = $otherUser->getProfile();
143 $oprofile = Ostatus_profile::ensureActivityObjectProfile($other);
144 $otherProfile = $oprofile->localProfile();
147 Subscription::start($profile, $otherProfile);
149 throw new Exception("This activity seems unrelated to our user.");
153 function joinGroup($user, $activity)
155 // XXX: check that actor == subject
157 $uri = $activity->objects[0]->id;
159 $group = User_group::staticGet('uri', $uri);
162 $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]);
163 if (!$oprofile->isGroup()) {
164 throw new Exception("Remote profile is not a group!");
166 $group = $oprofile->localGroup();
169 assert(!empty($group));
171 if (Event::handle('StartJoinGroup', array($group, $user))) {
172 Group_member::join($group->id, $user->id);
173 Event::handle('EndJoinGroup', array($group, $user));
177 // XXX: largely cadged from Ostatus_profile::processNote()
179 function postNote($user, $activity)
181 $note = $activity->objects[0];
183 $sourceUri = $note->id;
185 $notice = Notice::staticGet('uri', $sourceUri);
187 if (!empty($notice)) {
189 $orig = clone($notice);
190 $notice->profile_id = $user->id;
191 $notice->update($orig);
195 // Use summary as fallback for content
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;
204 // @fixme fetch from $sourceUrl?
205 // @todo i18n FIXME: use sprintf and add i18n.
206 throw new ClientException("No content for notice {$sourceUri}.");
209 // Get (safe!) HTML and text versions of the content
211 $rendered = $this->purify($sourceContent);
212 $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8');
214 $shortened = $user->shortenLinks($content);
216 $options = array('is_local' => Notice::LOCAL_PUBLIC,
218 'rendered' => $rendered,
219 'replies' => array(),
224 // Check for optional attributes...
226 if (!empty($activity->time)) {
227 $options['created'] = common_sql_date($activity->time);
230 if ($activity->context) {
231 // Any individual or group attn: targets?
233 list($options['groups'], $options['replies']) = $this->filterAttention($activity->context->attention);
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);
241 $options['reply_to'] = $orig->id;
245 $location = $activity->context->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;
257 // Atom categories <-> hashtags
259 foreach ($activity->categories as $cat) {
261 $term = common_canonical_tag($cat->term);
263 $options['tags'][] = $term;
268 // Atom enclosures -> attachment URLs
269 foreach ($activity->enclosures as $href) {
270 // @fixme save these locally or....?
271 $options['urls'][] = $href;
274 $saved = Notice::saveNew($user->id,
276 'restore', // TODO: restore the actual source
282 function filterAttention($attn)
287 foreach (array_unique($attn) as $recipient) {
289 // Is the recipient a local user?
291 $user = User::staticGet('uri', $recipient);
294 // @fixme sender verification, spam etc?
295 $replies[] = $recipient;
299 // Is the recipient a remote group?
300 $oprofile = Ostatus_profile::ensureProfileURI($recipient);
303 if (!$oprofile->isGroup()) {
304 // may be canonicalized or something
305 $replies[] = $oprofile->uri;
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);
316 $group = User_group::staticGet('id', $id);
318 // Deliver to all members of this local group if allowed.
319 $profile = $sender->localProfile();
320 if ($profile->isMember($group)) {
321 $groups[] = $group->id;
323 common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member");
327 common_log(LOG_INFO, "Skipping reply to bogus group $recipient");
332 return array($groups, $replies);
335 function userFromSubject($subject)
337 $user = User::staticGet('uri', $subject->id);
341 array('nickname' => Ostatus_profile::getActivityObjectNickname($subject),
342 'uri' => $subject->id);
344 $user = User::register($attrs);
347 $profile = $user->getProfile();
348 Ostatus_profile::updateProfile($profile, $subject);
350 // FIXME: Update avatar
354 function purify($content)
356 $config = array('safe' => 1,
357 'deny_attribute' => 'id,style,on*');
358 return htmLawed($content, $config);