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 private $_trusted = false;
57 function loadXML($xml)
59 $dom = DOMDocument::loadXML($xml);
61 if ($dom->documentElement->namespaceURI != Activity::ATOM ||
62 $dom->documentElement->localName != 'feed') {
63 throw new Exception("'$filename' is not an Atom feed.");
69 function importActivityStream($user, $doc)
71 $feed = $doc->documentElement;
73 $subjectEl = ActivityUtils::child($feed, Activity::SUBJECT, Activity::SPEC);
75 if (!empty($subjectEl)) {
76 $subject = new ActivityObject($subjectEl);
78 throw new Exception("Feed doesn't have an <activity:subject> element.");
82 $user = $this->userFromSubject($subject);
85 $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
87 $activities = $this->entriesToActivities($entries, $feed);
89 // XXX: sort entries here
91 foreach ($activities as $activity) {
93 switch ($activity->verb) {
94 case ActivityVerb::FOLLOW:
95 $this->subscribeProfile($user, $subject, $activity);
97 case ActivityVerb::JOIN:
98 $this->joinGroup($user, $activity);
100 case ActivityVerb::POST:
101 $this->postNote($user, $activity);
104 throw new Exception("Unknown verb: {$activity->verb}");
106 } catch (Exception $e) {
107 common_log(LOG_WARNING, $e->getMessage());
113 function subscribeProfile($user, $subject, $activity)
115 $profile = $user->getProfile();
117 if ($activity->objects[0]->id == $subject->id) {
118 if (!$this->_trusted) {
119 throw new Exception("Skipping a pushed subscription.");
121 $other = $activity->actor;
122 $otherUser = User::staticGet('uri', $other->id);
124 if (!empty($otherUser)) {
125 $otherProfile = $otherUser->getProfile();
127 throw new Exception("Can't force remote user to subscribe.");
129 // XXX: don't do this for untrusted input!
130 Subscription::start($otherProfile, $profile);
132 } else if (empty($activity->actor)
133 || $activity->actor->id == $subject->id) {
135 $other = $activity->objects[0];
136 $otherUser = User::staticGet('uri', $other->id);
138 if (!empty($otherUser)) {
139 $otherProfile = $otherUser->getProfile();
141 $oprofile = Ostatus_profile::ensureActivityObjectProfile($other);
142 $otherProfile = $oprofile->localProfile();
145 Subscription::start($profile, $otherProfile);
147 throw new Exception("This activity seems unrelated to our user.");
151 function joinGroup($user, $activity)
153 // XXX: check that actor == subject
155 $uri = $activity->objects[0]->id;
157 $group = User_group::staticGet('uri', $uri);
160 $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]);
161 if (!$oprofile->isGroup()) {
162 throw new Exception("Remote profile is not a group!");
164 $group = $oprofile->localGroup();
167 assert(!empty($group));
169 if (Event::handle('StartJoinGroup', array($group, $user))) {
170 Group_member::join($group->id, $user->id);
171 Event::handle('EndJoinGroup', array($group, $user));
175 // XXX: largely cadged from Ostatus_profile::processNote()
177 function postNote($user, $activity)
179 $note = $activity->objects[0];
181 $sourceUri = $note->id;
183 $notice = Notice::staticGet('uri', $sourceUri);
185 if (!empty($notice)) {
187 $orig = clone($notice);
188 $notice->profile_id = $user->id;
189 $notice->update($orig);
193 // Use summary as fallback for content
195 if (!empty($note->content)) {
196 $sourceContent = $note->content;
197 } else if (!empty($note->summary)) {
198 $sourceContent = $note->summary;
199 } else if (!empty($note->title)) {
200 $sourceContent = $note->title;
202 // @fixme fetch from $sourceUrl?
203 // @todo i18n FIXME: use sprintf and add i18n.
204 throw new ClientException("No content for notice {$sourceUri}.");
207 // Get (safe!) HTML and text versions of the content
209 $rendered = $this->purify($sourceContent);
210 $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8');
212 $shortened = $user->shortenLinks($content);
214 $options = array('is_local' => Notice::LOCAL_PUBLIC,
216 'rendered' => $rendered,
217 'replies' => array(),
222 // Check for optional attributes...
224 if (!empty($activity->time)) {
225 $options['created'] = common_sql_date($activity->time);
228 if ($activity->context) {
229 // Any individual or group attn: targets?
231 list($options['groups'], $options['replies']) = $this->filterAttention($activity->context->attention);
233 // Maintain direct reply associations
234 // @fixme what about conversation ID?
235 if (!empty($activity->context->replyToID)) {
236 $orig = Notice::staticGet('uri',
237 $activity->context->replyToID);
239 $options['reply_to'] = $orig->id;
243 $location = $activity->context->location;
246 $options['lat'] = $location->lat;
247 $options['lon'] = $location->lon;
248 if ($location->location_id) {
249 $options['location_ns'] = $location->location_ns;
250 $options['location_id'] = $location->location_id;
255 // Atom categories <-> hashtags
257 foreach ($activity->categories as $cat) {
259 $term = common_canonical_tag($cat->term);
261 $options['tags'][] = $term;
266 // Atom enclosures -> attachment URLs
267 foreach ($activity->enclosures as $href) {
268 // @fixme save these locally or....?
269 $options['urls'][] = $href;
272 $saved = Notice::saveNew($user->id,
274 'restore', // TODO: restore the actual source
280 function filterAttention($attn)
285 foreach (array_unique($attn) as $recipient) {
287 // Is the recipient a local user?
289 $user = User::staticGet('uri', $recipient);
292 // @fixme sender verification, spam etc?
293 $replies[] = $recipient;
297 // Is the recipient a remote group?
298 $oprofile = Ostatus_profile::ensureProfileURI($recipient);
301 if (!$oprofile->isGroup()) {
302 // may be canonicalized or something
303 $replies[] = $oprofile->uri;
308 // Is the recipient a local group?
309 // @fixme uri on user_group isn't reliable yet
310 // $group = User_group::staticGet('uri', $recipient);
311 $id = OStatusPlugin::localGroupFromUrl($recipient);
314 $group = User_group::staticGet('id', $id);
316 // Deliver to all members of this local group if allowed.
317 $profile = $sender->localProfile();
318 if ($profile->isMember($group)) {
319 $groups[] = $group->id;
321 common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member");
325 common_log(LOG_INFO, "Skipping reply to bogus group $recipient");
330 return array($groups, $replies);
333 function userFromSubject($subject)
335 $user = User::staticGet('uri', $subject->id);
339 array('nickname' => Ostatus_profile::getActivityObjectNickname($subject),
340 'uri' => $subject->id);
342 $user = User::register($attrs);
345 $profile = $user->getProfile();
346 Ostatus_profile::updateProfile($profile, $subject);
348 // FIXME: Update avatar
352 function purify($content)
354 $config = array('safe' => 1,
355 'deny_attribute' => 'id,style,on*');
356 return htmLawed($content, $config);