From 7c7b91e61ad273023d774617f23fa1429f535b22 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 13 Dec 2010 16:28:02 -0500 Subject: define configuration settings for account maintenance security --- README | 6 ++++++ lib/default.php | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README b/README index 3bebb11ee..e2e4c580e 100644 --- a/README +++ b/README @@ -1276,6 +1276,12 @@ Profile management. biolimit: max character length of bio; 0 means no limit; null means to use the site text limit default. +backup: whether users can backup their own profiles. Defaults to true. +restore: whether users can restore their profiles from backup files. Defaults + to true. +delete: whether users can delete their own accounts. Defaults to true. +move: whether users can move their accounts to another server. Defaults + to true. newuser ------- diff --git a/lib/default.php b/lib/default.php index 029dbb390..7a44ed875 100644 --- a/lib/default.php +++ b/lib/default.php @@ -123,7 +123,11 @@ $default = 'featured' => array()), 'profile' => array('banned' => array(), - 'biolimit' => null), + 'biolimit' => null, + 'backup' => true, + 'restore' => true, + 'delete' => true, + 'move' => true), 'avatar' => array('server' => null, 'dir' => INSTALLDIR . '/avatar/', -- cgit v1.2.3 From 75aaa9846263cb25d0047200e9eec678ca725ffe Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 13 Dec 2010 16:28:32 -0500 Subject: define rights for account maintenance and default rules --- classes/Profile.php | 12 ++++++++++++ lib/right.php | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/classes/Profile.php b/classes/Profile.php index 332d51e20..b83337cd2 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -891,6 +891,18 @@ class Profile extends Memcached_DataObject case Right::EMAILONFAVE: $result = !$this->isSandboxed(); break; + case Right::BACKUPACCOUNT: + $result = common_config('profile', 'backup'); + break; + case Right::RESTOREACCOUNT: + $result = common_config('profile', 'restore'); + break; + case Right::DELETEACCOUNT: + $result = common_config('profile', 'delete'); + break; + case Right::MOVEACCOUNT: + $result = common_config('profile', 'move'); + break; default: $result = false; break; diff --git a/lib/right.php b/lib/right.php index bacbea5f2..5bf9c4116 100644 --- a/lib/right.php +++ b/lib/right.php @@ -61,5 +61,9 @@ class Right const GRANTROLE = 'grantrole'; const REVOKEROLE = 'revokerole'; const DELETEGROUP = 'deletegroup'; + const BACKUPACCOUNT = 'backupaccount'; + const RESTOREACCOUNT = 'restoreaccount'; + const DELETEACCOUNT = 'deleteaccount'; + const MOVEACCOUNT = 'moveaccount'; } -- cgit v1.2.3 From 5089d3065c5b2944e468d821a0afdc0881830445 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 13 Dec 2010 16:32:39 -0500 Subject: add an action to backup the current account in ActivityStreams format --- actions/backupaccount.php | 260 ++++++++++++++++++++++++++++++++++++++++++++ actions/profilesettings.php | 9 ++ lib/router.php | 1 + 3 files changed, 270 insertions(+) create mode 100644 actions/backupaccount.php diff --git a/actions/backupaccount.php b/actions/backupaccount.php new file mode 100644 index 000000000..9454741f0 --- /dev/null +++ b/actions/backupaccount.php @@ -0,0 +1,260 @@ +. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Download a backup of your own account to the browser + * + * We go through some hoops to make this only respond to POST, since + * it's kind of expensive and there's probably some downside to having + * your account in all kinds of search engines. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class BackupaccountAction extends Action +{ + /** + * Returns the title of the page + * + * @return string page title + */ + + function title() + { + return _("Backup account"); + } + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + $cur = common_current_user(); + + if (empty($cur)) { + throw new ClientException(_('Only logged-in users can backup their account.'), 403); + } + + if (!$cur->hasRight(Right::BACKUPACCOUNT)) { + throw new ClientException(_('You may not backup your account.'), 403); + } + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + + function handle($argarray=null) + { + parent::handle($args); + + if ($this->isPost()) { + $this->sendFeed(); + } else { + $this->showPage(); + } + return; + } + + /** + * Send a feed of the user's activities to the browser + * + * Uses the UserActivityStream class; may take a long time! + * + * @return void + */ + + function sendFeed() + { + $cur = common_current_user(); + + $stream = new UserActivityStream($cur); + + header('Content-Disposition: attachment; filename='.$cur->nickname.'.atom'); + header('Content-Type: application/atom+xml; charset=utf-8'); + + $this->raw($stream->getString()); + } + + /** + * Show a little form so that the person can request a backup. + * + * @return void + */ + + function showContent() + { + $form = new BackupAccountForm($this); + $form->show(); + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + return false; + } + + /** + * Return last modified, if applicable. + * + * MAY override + * + * @return string last modified http header + */ + + function lastModified() + { + // For comparison with If-Last-Modified + // If not applicable, return null + return null; + } + + /** + * Return etag, if applicable. + * + * MAY override + * + * @return string etag http header + */ + + function etag() + { + return null; + } +} + +/** + * A form for backing up the account. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class BackupAccountForm extends Form +{ + /** + * Class of the form. + * + * @return string the form's class + */ + + function formClass() + { + return 'form_profile_backup'; + } + + /** + * URL the form posts to + * + * @return string the form's action URL + */ + + function action() + { + return common_local_url('backupaccount'); + } + + /** + * Output form data + * + * Really, just instructions for doing a backup. + * + * @return void + */ + + function formData() + { + $msg = + _('You can backup your account data in '. + 'Activity Streams '. + 'format. This is an experimental feature and provides an '. + 'incomplete backup; private account '. + 'information like email and IM addresses is not backed up. '. + 'Additionally, uploaded files and direct messages are not '. + 'backed up.'); + $this->out->elementStart('p'); + $this->out->raw($msg); + $this->out->elementEnd('p'); + } + + /** + * Buttons for the form + * + * In this case, a single submit button + * + * @return void + */ + + function formActions() + { + $this->out->submit('submit', + _m('BUTTON', 'Backup'), + 'submit', + null, + _('Backup your account')); + } +} diff --git a/actions/profilesettings.php b/actions/profilesettings.php index 28b1d20f3..17ffdf811 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -452,4 +452,13 @@ class ProfilesettingsAction extends AccountSettingsAction return $other->id != $user->id; } } + + function showAside() { + $this->elementStart('div', array('id' => 'aside_primary', + 'class' => 'aside')); + $this->element('a', + array('href' => common_local_url('backupaccount')), + _('Backup account')); + $this->elementEnd('div'); + } } diff --git a/lib/router.php b/lib/router.php index c42cca5f6..369eebf8b 100644 --- a/lib/router.php +++ b/lib/router.php @@ -199,6 +199,7 @@ class Router 'deleteuser', 'geocode', 'version', + 'backupaccount', ); foreach ($main as $a) { -- cgit v1.2.3 From 6a7bf9dbf97a86881181d070894b0586d9d34129 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 13 Dec 2010 16:49:01 -0500 Subject: don't show the backup link if the user can't backup --- actions/profilesettings.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/actions/profilesettings.php b/actions/profilesettings.php index 17ffdf811..4890a575b 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -454,11 +454,15 @@ class ProfilesettingsAction extends AccountSettingsAction } function showAside() { + $user = common_current_user(); + $this->elementStart('div', array('id' => 'aside_primary', 'class' => 'aside')); - $this->element('a', - array('href' => common_local_url('backupaccount')), - _('Backup account')); + if ($user->hasRight(Right::BACKUPACCOUNT)) { + $this->element('a', + array('href' => common_local_url('backupaccount')), + _('Backup account')); + } $this->elementEnd('div'); } } -- cgit v1.2.3 From d840578aa0ad6284f57591aae87f87865905db3c Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 14 Dec 2010 12:38:43 -0500 Subject: An action to delete your own account The new DeleteaccountAction lets a user delete their own account (subject to global rights set by the admin). It presents a form to delete the account, with an "I am sure." text entry box. It then schedules the account for deletion and logs the user out. --- actions/deleteaccount.php | 319 ++++++++++++++++++++++++++++++++++++++++++++ actions/profilesettings.php | 9 ++ lib/router.php | 1 + 3 files changed, 329 insertions(+) create mode 100644 actions/deleteaccount.php diff --git a/actions/deleteaccount.php b/actions/deleteaccount.php new file mode 100644 index 000000000..c7dfa570c --- /dev/null +++ b/actions/deleteaccount.php @@ -0,0 +1,319 @@ +. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Action to delete your own account + * + * Note that this is distinct from DeleteuserAction, which see. I thought + * that making that action do both things (delete another user and delete the + * current user) would open a lot of holes. I'm open to refactoring, however. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class DeleteaccountAction extends Action +{ + private $_complete = false; + private $_error = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + $cur = common_current_user(); + + if (empty($cur)) { + throw new ClientException(_("Only logged-in users ". + "can delete their account."), 403); + } + + if (!$cur->hasRight(Right::DELETEACCOUNT)) { + throw new ClientException(_("You cannot delete your account."), 403); + } + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + + function handle($argarray=null) + { + parent::handle($argarray); + + if ($this->isPost()) { + $this->deleteAccount(); + } else { + $this->showPage(); + } + return; + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + return false; + } + + /** + * Return last modified, if applicable. + * + * MAY override + * + * @return string last modified http header + */ + + function lastModified() + { + // For comparison with If-Last-Modified + // If not applicable, return null + return null; + } + + /** + * Return etag, if applicable. + * + * MAY override + * + * @return string etag http header + */ + + function etag() + { + return null; + } + + /** + * Delete the current user's account + * + * Checks for the "I am sure." string to make sure the user really + * wants to delete their account. + * + * Then, marks the account as deleted and begins the deletion process + * (actually done by a back-end handler). + * + * If successful it logs the user out, and shows a brief completion message. + * + * @return void + */ + + function deleteAccount() + { + $this->checkSessionToken(); + + if ($this->trimmed('iamsure') != _('I am sure.')) { + $this->_error = _('You must write "I am sure." exactly in the box.'); + $this->showPage(); + return; + } + + $cur = common_current_user(); + + // Mark the account as deleted and shove low-level deletion tasks + // to background queues. Removing a lot of posts can take a while... + + if (!$cur->hasRole(Profile_role::DELETED)) { + $cur->grantRole(Profile_role::DELETED); + } + + $qm = QueueManager::get(); + $qm->enqueue($cur, 'deluser'); + + // The user is really-truly logged out + + common_set_user(null); + common_real_login(false); // not logged in + common_forgetme(); // don't log back in! + + $this->_complete = true; + $this->showPage(); + } + + /** + * Shows the page content. + * + * If the deletion is complete, just shows a completion message. + * + * Otherwise, shows the deletion form. + * + * @return void + * + */ + + function showContent() + { + if ($this->_complete) { + $this->element('p', 'confirmation', + _('Account deleted.')); + return; + } + + if (!empty($this->_error)) { + $this->element('p', 'error', $this->_error); + $this->_error = null; + } + + $form = new DeleteAccountForm($this); + $form->show(); + } + + /** + * Show the title of the page + * + * @return string title + */ + + function title() + { + return _('Delete account'); + } +} + +/** + * Form for deleting your account + * + * Note that this mostly is here to keep you from accidentally deleting your + * account. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class DeleteAccountForm extends Form +{ + /** + * Class of the form. + * + * @return string the form's class + */ + + function formClass() + { + return 'form_profile_delete'; + } + + /** + * URL the form posts to + * + * @return string the form's action URL + */ + + function action() + { + return common_local_url('deleteaccount'); + } + + /** + * Output form data + * + * Instructions plus an 'i am sure' entry box. + * + * @return void + */ + + function formData() + { + $cur = common_current_user(); + + $msg = _('

This will permanently delete '. + 'your account data from this server.

'); + + if ($cur->hasRight(Right::BACKUPACCOUNT)) { + $msg .= sprintf(_('

You are strongly advised to '. + 'back up your data' + ' before deletion.

'), + common_local_url('backupaccount')); + } + + $this->out->elementStart('p'); + $this->out->raw($msg); + $this->out->elementEnd('p'); + + $this->out->input('iamsure', + _('Confirm'), + null, + _('Enter "I am sure." to confirm that '. + 'you want to delete your account.')); + } + + /** + * Buttons for the form + * + * In this case, a single submit button + * + * @return void + */ + + function formActions() + { + $this->out->submit('submit', + _m('BUTTON', 'Delete'), + 'submit', + null, + _('Permanently your account')); + } +} diff --git a/actions/profilesettings.php b/actions/profilesettings.php index 4890a575b..0226e1dd4 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -459,9 +459,18 @@ class ProfilesettingsAction extends AccountSettingsAction $this->elementStart('div', array('id' => 'aside_primary', 'class' => 'aside')); if ($user->hasRight(Right::BACKUPACCOUNT)) { + $this->elementStart('li'); $this->element('a', array('href' => common_local_url('backupaccount')), _('Backup account')); + $this->elementEnd('li'); + } + if ($user->hasRight(Right::DELETEACCOUNT)) { + $this->elementStart('li'); + $this->element('a', + array('href' => common_local_url('deleteaccount')), + _('Delete account')); + $this->elementEnd('li'); } $this->elementEnd('div'); } diff --git a/lib/router.php b/lib/router.php index 369eebf8b..fc5f17cde 100644 --- a/lib/router.php +++ b/lib/router.php @@ -200,6 +200,7 @@ class Router 'geocode', 'version', 'backupaccount', + 'deleteaccount', ); foreach ($main as $a) { -- cgit v1.2.3 From fd22f684bf24e2c3d5ea8af0b11de8127c3b7b99 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 15 Dec 2010 17:39:58 -0500 Subject: syntax error in deleteaccount --- actions/deleteaccount.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/deleteaccount.php b/actions/deleteaccount.php index c7dfa570c..9abe2fcdb 100644 --- a/actions/deleteaccount.php +++ b/actions/deleteaccount.php @@ -284,7 +284,7 @@ class DeleteAccountForm extends Form if ($cur->hasRight(Right::BACKUPACCOUNT)) { $msg .= sprintf(_('

You are strongly advised to '. - 'back up your data' + 'back up your data'. ' before deletion.

'), common_local_url('backupaccount')); } -- cgit v1.2.3 From 2e2519afee87009165c97026737f72634461e82b Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 15 Dec 2010 17:53:38 -0500 Subject: Move account restoration code to a shared library Moved most of the heavy-lifting for account restoration out of restoreuser.php and into its own class, with the hope that we'll do the work from the Web eventually. --- lib/accountrestorer.php | 360 ++++++++++++++++++++++++++++++++++++++++++++++++ scripts/restoreuser.php | 310 +---------------------------------------- 2 files changed, 367 insertions(+), 303 deletions(-) create mode 100644 lib/accountrestorer.php diff --git a/lib/accountrestorer.php b/lib/accountrestorer.php new file mode 100644 index 000000000..98f12ccb6 --- /dev/null +++ b/lib/accountrestorer.php @@ -0,0 +1,360 @@ +. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * A class for restoring accounts + * + * This is a clumsy objectification of the functions in restoreuser.php. + * + * Note that it quite illegally uses the OStatus_profile class which may + * not even exist on this server. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class AccountRestorer +{ + function loadXML($xml) + { + $dom = DOMDocument::loadXML($xml); + + if ($dom->documentElement->namespaceURI != Activity::ATOM || + $dom->documentElement->localName != 'feed') { + throw new Exception("'$filename' is not an Atom feed."); + } + + return $dom; + } + + function importActivityStream($user, $doc) + { + $feed = $doc->documentElement; + + $subjectEl = ActivityUtils::child($feed, Activity::SUBJECT, Activity::SPEC); + + if (!empty($subjectEl)) { + $subject = new ActivityObject($subjectEl); + // TRANS: Commandline script output. %1$s is the subject ID, %2$s is the subject nickname. + printfv(_("Backup file for user %1$s (%2$s)")."\n", $subject->id, Ostatus_profile::getActivityObjectNickname($subject)); + } else { + throw new Exception("Feed doesn't have an element."); + } + + if (is_null($user)) { + // TRANS: Commandline script output. + printfv(_("No user specified; using backup user.")."\n"); + $user = $this->userFromSubject($subject); + } + + $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); + + // TRANS: Commandline script output. %d is the number of entries in the activity stream in backup; used for plural. + printfv(_m("%d entry in backup.","%d entries in backup.",$entries->length)."\n", $entries->length); + + for ($i = $entries->length - 1; $i >= 0; $i--) { + try { + $entry = $entries->item($i); + + $activity = new Activity($entry, $feed); + + switch ($activity->verb) { + case ActivityVerb::FOLLOW: + $this->subscribeProfile($user, $subject, $activity); + break; + case ActivityVerb::JOIN: + $this->joinGroup($user, $activity); + break; + case ActivityVerb::POST: + $this->postNote($user, $activity); + break; + default: + throw new Exception("Unknown verb: {$activity->verb}"); + } + } catch (Exception $e) { + print $e->getMessage()."\n"; + continue; + } + } + } + + function subscribeProfile($user, $subject, $activity) + { + $profile = $user->getProfile(); + + if ($activity->objects[0]->id == $subject->id) { + + $other = $activity->actor; + $otherUser = User::staticGet('uri', $other->id); + + if (!empty($otherUser)) { + $otherProfile = $otherUser->getProfile(); + } else { + throw new Exception("Can't force remote user to subscribe."); + } + // XXX: don't do this for untrusted input! + Subscription::start($otherProfile, $profile); + + } else if (empty($activity->actor) || $activity->actor->id == $subject->id) { + + $other = $activity->objects[0]; + $otherUser = User::staticGet('uri', $other->id); + + if (!empty($otherUser)) { + $otherProfile = $otherUser->getProfile(); + } else { + $oprofile = Ostatus_profile::ensureActivityObjectProfile($other); + $otherProfile = $oprofile->localProfile(); + } + + Subscription::start($profile, $otherProfile); + } else { + throw new Exception("This activity seems unrelated to our user."); + } + } + + function joinGroup($user, $activity) + { + // XXX: check that actor == subject + + $uri = $activity->objects[0]->id; + + $group = User_group::staticGet('uri', $uri); + + if (empty($group)) { + $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]); + if (!$oprofile->isGroup()) { + throw new Exception("Remote profile is not a group!"); + } + $group = $oprofile->localGroup(); + } + + assert(!empty($group)); + + if (Event::handle('StartJoinGroup', array($group, $user))) { + Group_member::join($group->id, $user->id); + Event::handle('EndJoinGroup', array($group, $user)); + } + } + + // XXX: largely cadged from Ostatus_profile::processNote() + + function postNote($user, $activity) + { + $note = $activity->objects[0]; + + $sourceUri = $note->id; + + $notice = Notice::staticGet('uri', $sourceUri); + + if (!empty($notice)) { + // This is weird. + $orig = clone($notice); + $notice->profile_id = $user->id; + $notice->update($orig); + return; + } + + // Use summary as fallback for content + + if (!empty($note->content)) { + $sourceContent = $note->content; + } else if (!empty($note->summary)) { + $sourceContent = $note->summary; + } else if (!empty($note->title)) { + $sourceContent = $note->title; + } else { + // @fixme fetch from $sourceUrl? + // @todo i18n FIXME: use sprintf and add i18n. + throw new ClientException("No content for notice {$sourceUri}."); + } + + // Get (safe!) HTML and text versions of the content + + $rendered = $this->purify($sourceContent); + $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8'); + + $shortened = $user->shortenLinks($content); + + $options = array('is_local' => Notice::LOCAL_PUBLIC, + 'uri' => $sourceUri, + 'rendered' => $rendered, + 'replies' => array(), + 'groups' => array(), + 'tags' => array(), + 'urls' => array()); + + // Check for optional attributes... + + if (!empty($activity->time)) { + $options['created'] = common_sql_date($activity->time); + } + + if ($activity->context) { + // Any individual or group attn: targets? + + list($options['groups'], $options['replies']) = $this->filterAttention($activity->context->attention); + + // Maintain direct reply associations + // @fixme what about conversation ID? + if (!empty($activity->context->replyToID)) { + $orig = Notice::staticGet('uri', + $activity->context->replyToID); + if (!empty($orig)) { + $options['reply_to'] = $orig->id; + } + } + + $location = $activity->context->location; + + if ($location) { + $options['lat'] = $location->lat; + $options['lon'] = $location->lon; + if ($location->location_id) { + $options['location_ns'] = $location->location_ns; + $options['location_id'] = $location->location_id; + } + } + } + + // Atom categories <-> hashtags + + foreach ($activity->categories as $cat) { + if ($cat->term) { + $term = common_canonical_tag($cat->term); + if ($term) { + $options['tags'][] = $term; + } + } + } + + // Atom enclosures -> attachment URLs + foreach ($activity->enclosures as $href) { + // @fixme save these locally or....? + $options['urls'][] = $href; + } + + $saved = Notice::saveNew($user->id, + $content, + 'restore', // TODO: restore the actual source + $options); + + return $saved; + } + + function filterAttention($attn) + { + $groups = array(); + $replies = array(); + + foreach (array_unique($attn) as $recipient) { + + // Is the recipient a local user? + + $user = User::staticGet('uri', $recipient); + + if ($user) { + // @fixme sender verification, spam etc? + $replies[] = $recipient; + continue; + } + + // Is the recipient a remote group? + $oprofile = Ostatus_profile::ensureProfileURI($recipient); + + if ($oprofile) { + if (!$oprofile->isGroup()) { + // may be canonicalized or something + $replies[] = $oprofile->uri; + } + continue; + } + + // Is the recipient a local group? + // @fixme uri on user_group isn't reliable yet + // $group = User_group::staticGet('uri', $recipient); + $id = OStatusPlugin::localGroupFromUrl($recipient); + + if ($id) { + $group = User_group::staticGet('id', $id); + if ($group) { + // Deliver to all members of this local group if allowed. + $profile = $sender->localProfile(); + if ($profile->isMember($group)) { + $groups[] = $group->id; + } else { + common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member"); + } + continue; + } else { + common_log(LOG_INFO, "Skipping reply to bogus group $recipient"); + } + } + } + + return array($groups, $replies); + } + + function userFromSubject($subject) + { + $user = User::staticGet('uri', $subject->id); + + if (empty($user)) { + $attrs = + array('nickname' => Ostatus_profile::getActivityObjectNickname($subject), + 'uri' => $subject->id); + + $user = User::register($attrs); + } + + $profile = $user->getProfile(); + Ostatus_profile::updateProfile($profile, $subject); + + // FIXME: Update avatar + return $user; + } + + function purify($content) + { + $config = array('safe' => 1, + 'deny_attribute' => 'id,style,on*'); + return htmLawed($content, $config); + } +} diff --git a/scripts/restoreuser.php b/scripts/restoreuser.php index b37e9db74..eac7e5cf2 100644 --- a/scripts/restoreuser.php +++ b/scripts/restoreuser.php @@ -36,6 +36,7 @@ END_OF_RESTOREUSER_HELP; require_once INSTALLDIR.'/scripts/commandline.inc'; require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php'; + function getActivityStreamDocument() { $filename = get_option_value('f', 'file'); @@ -60,311 +61,12 @@ function getActivityStreamDocument() // TRANS: Commandline script output. %s is the filename that contains a backup for a user. printfv(_("Getting backup from file '%s'.")."\n",$filename); - $xml = file_get_contents($filename); - - $dom = DOMDocument::loadXML($xml); - - if ($dom->documentElement->namespaceURI != Activity::ATOM || - $dom->documentElement->localName != 'feed') { - throw new Exception("'$filename' is not an Atom feed."); - } - - return $dom; -} - -function importActivityStream($user, $doc) -{ - $feed = $doc->documentElement; - - $subjectEl = ActivityUtils::child($feed, Activity::SUBJECT, Activity::SPEC); - - if (!empty($subjectEl)) { - $subject = new ActivityObject($subjectEl); - // TRANS: Commandline script output. %1$s is the subject ID, %2$s is the subject nickname. - printfv(_("Backup file for user %1$s (%2$s)")."\n", $subject->id, Ostatus_profile::getActivityObjectNickname($subject)); - } else { - throw new Exception("Feed doesn't have an element."); - } - - if (is_null($user)) { - // TRANS: Commandline script output. - printfv(_("No user specified; using backup user.")."\n"); - $user = userFromSubject($subject); - } - - $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); - - // TRANS: Commandline script output. %d is the number of entries in the activity stream in backup; used for plural. - printfv(_m("%d entry in backup.","%d entries in backup.",$entries->length)."\n", $entries->length); - - for ($i = $entries->length - 1; $i >= 0; $i--) { - try { - $entry = $entries->item($i); - - $activity = new Activity($entry, $feed); - - switch ($activity->verb) { - case ActivityVerb::FOLLOW: - subscribeProfile($user, $subject, $activity); - break; - case ActivityVerb::JOIN: - joinGroup($user, $activity); - break; - case ActivityVerb::POST: - postNote($user, $activity); - break; - default: - throw new Exception("Unknown verb: {$activity->verb}"); - } - } catch (Exception $e) { - print $e->getMessage()."\n"; - continue; - } - } -} - -function subscribeProfile($user, $subject, $activity) -{ - $profile = $user->getProfile(); - - if ($activity->objects[0]->id == $subject->id) { - - $other = $activity->actor; - $otherUser = User::staticGet('uri', $other->id); - - if (!empty($otherUser)) { - $otherProfile = $otherUser->getProfile(); - } else { - throw new Exception("Can't force remote user to subscribe."); - } - // XXX: don't do this for untrusted input! - Subscription::start($otherProfile, $profile); - - } else if (empty($activity->actor) || $activity->actor->id == $subject->id) { - - $other = $activity->objects[0]; - $otherUser = User::staticGet('uri', $other->id); - - if (!empty($otherUser)) { - $otherProfile = $otherUser->getProfile(); - } else { - $oprofile = Ostatus_profile::ensureActivityObjectProfile($other); - $otherProfile = $oprofile->localProfile(); - } - - Subscription::start($profile, $otherProfile); - } else { - throw new Exception("This activity seems unrelated to our user."); - } -} - -function joinGroup($user, $activity) -{ - // XXX: check that actor == subject - - $uri = $activity->objects[0]->id; - - $group = User_group::staticGet('uri', $uri); - - if (empty($group)) { - $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]); - if (!$oprofile->isGroup()) { - throw new Exception("Remote profile is not a group!"); - } - $group = $oprofile->localGroup(); - } - - assert(!empty($group)); - - if (Event::handle('StartJoinGroup', array($group, $user))) { - Group_member::join($group->id, $user->id); - Event::handle('EndJoinGroup', array($group, $user)); - } -} - -// XXX: largely cadged from Ostatus_profile::processNote() - -function postNote($user, $activity) -{ - $note = $activity->objects[0]; - - $sourceUri = $note->id; - - $notice = Notice::staticGet('uri', $sourceUri); - if (!empty($notice)) { - // This is weird. - $orig = clone($notice); - $notice->profile_id = $user->id; - $notice->update($orig); - return; - } - - // Use summary as fallback for content - - if (!empty($note->content)) { - $sourceContent = $note->content; - } else if (!empty($note->summary)) { - $sourceContent = $note->summary; - } else if (!empty($note->title)) { - $sourceContent = $note->title; - } else { - // @fixme fetch from $sourceUrl? - // @todo i18n FIXME: use sprintf and add i18n. - throw new ClientException("No content for notice {$sourceUri}."); - } - - // Get (safe!) HTML and text versions of the content - - $rendered = purify($sourceContent); - $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8'); - - $shortened = $user->shortenLinks($content); - - $options = array('is_local' => Notice::LOCAL_PUBLIC, - 'uri' => $sourceUri, - 'rendered' => $rendered, - 'replies' => array(), - 'groups' => array(), - 'tags' => array(), - 'urls' => array()); - - // Check for optional attributes... - - if (!empty($activity->time)) { - $options['created'] = common_sql_date($activity->time); - } - - if ($activity->context) { - // Any individual or group attn: targets? - - list($options['groups'], $options['replies']) = filterAttention($activity->context->attention); - - // Maintain direct reply associations - // @fixme what about conversation ID? - if (!empty($activity->context->replyToID)) { - $orig = Notice::staticGet('uri', - $activity->context->replyToID); - if (!empty($orig)) { - $options['reply_to'] = $orig->id; - } - } - - $location = $activity->context->location; - - if ($location) { - $options['lat'] = $location->lat; - $options['lon'] = $location->lon; - if ($location->location_id) { - $options['location_ns'] = $location->location_ns; - $options['location_id'] = $location->location_id; - } - } - } - - // Atom categories <-> hashtags - - foreach ($activity->categories as $cat) { - if ($cat->term) { - $term = common_canonical_tag($cat->term); - if ($term) { - $options['tags'][] = $term; - } - } - } - - // Atom enclosures -> attachment URLs - foreach ($activity->enclosures as $href) { - // @fixme save these locally or....? - $options['urls'][] = $href; - } - - $saved = Notice::saveNew($user->id, - $content, - 'restore', // TODO: restore the actual source - $options); - - return $saved; -} - -function filterAttention($attn) -{ - $groups = array(); - $replies = array(); - - foreach (array_unique($attn) as $recipient) { - - // Is the recipient a local user? - - $user = User::staticGet('uri', $recipient); - - if ($user) { - // @fixme sender verification, spam etc? - $replies[] = $recipient; - continue; - } - - // Is the recipient a remote group? - $oprofile = Ostatus_profile::ensureProfileURI($recipient); - - if ($oprofile) { - if (!$oprofile->isGroup()) { - // may be canonicalized or something - $replies[] = $oprofile->uri; - } - continue; - } - - // Is the recipient a local group? - // @fixme uri on user_group isn't reliable yet - // $group = User_group::staticGet('uri', $recipient); - $id = OStatusPlugin::localGroupFromUrl($recipient); - - if ($id) { - $group = User_group::staticGet('id', $id); - if ($group) { - // Deliver to all members of this local group if allowed. - $profile = $sender->localProfile(); - if ($profile->isMember($group)) { - $groups[] = $group->id; - } else { - common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member"); - } - continue; - } else { - common_log(LOG_INFO, "Skipping reply to bogus group $recipient"); - } - } - } - - return array($groups, $replies); -} - -function userFromSubject($subject) -{ - $user = User::staticGet('uri', $subject->id); - - if (empty($user)) { - $attrs = - array('nickname' => Ostatus_profile::getActivityObjectNickname($subject), - 'uri' => $subject->id); - - $user = User::register($attrs); - } - - $profile = $user->getProfile(); - Ostatus_profile::updateProfile($profile, $subject); + $xml = file_get_contents($filename); - // FIXME: Update avatar - return $user; + return $xml; } -function purify($content) -{ - $config = array('safe' => 1, - 'deny_attribute' => 'id,style,on*'); - return htmLawed($content, $config); -} try { try { @@ -372,8 +74,10 @@ try { } catch (NoUserArgumentException $noae) { $user = null; } - $doc = getActivityStreamDocument(); - importActivityStream($user, $doc); + $xml = getActivityStreamDocument(); + $restorer = new AccountRestorer(); + $doc = $restorer->loadXML($xml); + $restorer->importActivityStream($user, $doc); } catch (Exception $e) { print $e->getMessage()."\n"; exit(1); -- cgit v1.2.3 From 39804809dd67d72926d985f31164e6df334ee387 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 16 Dec 2010 16:17:38 -0500 Subject: distribute flag for Notice::saveNew() --- classes/Notice.php | 11 ++++++++--- lib/accountrestorer.php | 44 +++++++++++++++++++++----------------------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index a067cd374..d412c5f3a 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -234,6 +234,8 @@ class Notice extends Memcached_DataObject * in place of extracting # tags from content * array 'urls' list of attached/referred URLs to save with the * notice in place of extracting links from content + * boolean 'distribute' whether to distribute the notice, default true + * * @fixme tag override * * @return Notice @@ -243,7 +245,8 @@ class Notice extends Memcached_DataObject $defaults = array('uri' => null, 'url' => null, 'reply_to' => null, - 'repeat_of' => null); + 'repeat_of' => null, + 'distribute' => true); if (!empty($options)) { $options = $options + $defaults; @@ -426,8 +429,10 @@ class Notice extends Memcached_DataObject $notice->saveUrls(); } - // Prepare inbox delivery, may be queued to background. - $notice->distribute(); + if ($distribute) { + // Prepare inbox delivery, may be queued to background. + $notice->distribute(); + } return $notice; } diff --git a/lib/accountrestorer.php b/lib/accountrestorer.php index 98f12ccb6..3f6ac0da4 100644 --- a/lib/accountrestorer.php +++ b/lib/accountrestorer.php @@ -52,6 +52,8 @@ if (!defined('STATUSNET')) { class AccountRestorer { + private $_trusted = false; + function loadXML($xml) { $dom = DOMDocument::loadXML($xml); @@ -72,29 +74,22 @@ class AccountRestorer if (!empty($subjectEl)) { $subject = new ActivityObject($subjectEl); - // TRANS: Commandline script output. %1$s is the subject ID, %2$s is the subject nickname. - printfv(_("Backup file for user %1$s (%2$s)")."\n", $subject->id, Ostatus_profile::getActivityObjectNickname($subject)); } else { throw new Exception("Feed doesn't have an element."); } if (is_null($user)) { - // TRANS: Commandline script output. - printfv(_("No user specified; using backup user.")."\n"); $user = $this->userFromSubject($subject); } $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); - // TRANS: Commandline script output. %d is the number of entries in the activity stream in backup; used for plural. - printfv(_m("%d entry in backup.","%d entries in backup.",$entries->length)."\n", $entries->length); - - for ($i = $entries->length - 1; $i >= 0; $i--) { - try { - $entry = $entries->item($i); + $activities = $this->entriesToActivities($entries, $feed); - $activity = new Activity($entry, $feed); + // XXX: sort entries here + foreach ($activities as $activity) { + try { switch ($activity->verb) { case ActivityVerb::FOLLOW: $this->subscribeProfile($user, $subject, $activity); @@ -109,7 +104,7 @@ class AccountRestorer throw new Exception("Unknown verb: {$activity->verb}"); } } catch (Exception $e) { - print $e->getMessage()."\n"; + common_log(LOG_WARNING, $e->getMessage()); continue; } } @@ -120,19 +115,22 @@ class AccountRestorer $profile = $user->getProfile(); if ($activity->objects[0]->id == $subject->id) { - - $other = $activity->actor; - $otherUser = User::staticGet('uri', $other->id); - - if (!empty($otherUser)) { - $otherProfile = $otherUser->getProfile(); + if (!$this->_trusted) { + throw new Exception("Skipping a pushed subscription."); } else { - throw new Exception("Can't force remote user to subscribe."); - } - // XXX: don't do this for untrusted input! - Subscription::start($otherProfile, $profile); + $other = $activity->actor; + $otherUser = User::staticGet('uri', $other->id); - } else if (empty($activity->actor) || $activity->actor->id == $subject->id) { + if (!empty($otherUser)) { + $otherProfile = $otherUser->getProfile(); + } else { + throw new Exception("Can't force remote user to subscribe."); + } + // XXX: don't do this for untrusted input! + Subscription::start($otherProfile, $profile); + } + } else if (empty($activity->actor) + || $activity->actor->id == $subject->id) { $other = $activity->objects[0]; $otherUser = User::staticGet('uri', $other->id); -- cgit v1.2.3 From 16fc5314fbc4d7542e45c75af787ef906ae359ae Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 17 Dec 2010 13:09:37 -0500 Subject: move code to get an author object for a feed to a library from Ostatus_profile --- lib/activityutils.php | 47 ++++++++++++++++++++++++++ plugins/OStatus/classes/Ostatus_profile.php | 51 +++++------------------------ 2 files changed, 55 insertions(+), 43 deletions(-) diff --git a/lib/activityutils.php b/lib/activityutils.php index c462514c4..11befc0ed 100644 --- a/lib/activityutils.php +++ b/lib/activityutils.php @@ -270,4 +270,51 @@ class ActivityUtils return false; } + + static function getFeedAuthor($feedEl) + { + // Try the feed author + + $author = ActivityUtils::child($feedEl, Activity::AUTHOR, Activity::ATOM); + + if (!empty($author)) { + return new ActivityObject($author); + } + + // Try old and deprecated activity:subject + + $subject = ActivityUtils::child($feedEl, Activity::SUBJECT, Activity::SPEC); + + if (!empty($subject)) { + return new ActivityObject($subject); + } + + // Sheesh. Not a very nice feed! Let's try fingerpoken in the + // entries. + + $entries = $feedEl->getElementsByTagNameNS(Activity::ATOM, 'entry'); + + if (!empty($entries) && $entries->length > 0) { + + $entry = $entries->item(0); + + // Try the author + + $author = ActivityUtils::child($entry, Activity::AUTHOR, Activity::ATOM); + + if (!empty($author)) { + return new ActivityObject($author); + } + + // Try the (deprecated) activity:actor + + $actor = ActivityUtils::child($entry, Activity::ACTOR, Activity::SPEC); + + if (!empty($actor)) { + return new ActivityObject($actor); + } + } + + return null; + } } diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index e5b8939a9..77cf57a67 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -935,54 +935,19 @@ class Ostatus_profile extends Memcached_DataObject * @return Ostatus_profile * @throws Exception */ + public static function ensureAtomFeed($feedEl, $hints) { - // Try to get a profile from the feed activity:subject - - $subject = ActivityUtils::child($feedEl, Activity::SUBJECT, Activity::SPEC); - - if (!empty($subject)) { - $subjObject = new ActivityObject($subject); - return self::ensureActivityObjectProfile($subjObject, $hints); - } - - // Otherwise, try the feed author - - $author = ActivityUtils::child($feedEl, Activity::AUTHOR, Activity::ATOM); - - if (!empty($author)) { - $authorObject = new ActivityObject($author); - return self::ensureActivityObjectProfile($authorObject, $hints); - } - - // Sheesh. Not a very nice feed! Let's try fingerpoken in the - // entries. + $author = ActivityUtils::getFeedAuthor($feedEl); - $entries = $feedEl->getElementsByTagNameNS(Activity::ATOM, 'entry'); - - if (!empty($entries) && $entries->length > 0) { - - $entry = $entries->item(0); - - $actor = ActivityUtils::child($entry, Activity::ACTOR, Activity::SPEC); - - if (!empty($actor)) { - $actorObject = new ActivityObject($actor); - return self::ensureActivityObjectProfile($actorObject, $hints); - - } - - $author = ActivityUtils::child($entry, Activity::AUTHOR, Activity::ATOM); - - if (!empty($author)) { - $authorObject = new ActivityObject($author); - return self::ensureActivityObjectProfile($authorObject, $hints); - } + if (empty($author)) { + // XXX: make some educated guesses here + // TRANS: Feed sub exception. + throw new FeedSubException(_m('Can\'t find enough profile '. + 'information to make a feed.')); } - // XXX: make some educated guesses here - // TRANS: Feed sub exception. - throw new FeedSubException(_m('Can\'t find enough profile information to make a feed.')); + return self::ensureActivityObjectProfile($author, $hints); } /** -- cgit v1.2.3 From 6469d75fb0c2d148c8947e905545da89293b3236 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 17 Dec 2010 13:10:23 -0500 Subject: Move accountrestorer class to feed importer --- lib/accountrestorer.php | 358 ------------------------------------------------ lib/feedimporter.php | 358 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 358 insertions(+), 358 deletions(-) delete mode 100644 lib/accountrestorer.php create mode 100644 lib/feedimporter.php diff --git a/lib/accountrestorer.php b/lib/accountrestorer.php deleted file mode 100644 index 3f6ac0da4..000000000 --- a/lib/accountrestorer.php +++ /dev/null @@ -1,358 +0,0 @@ -. - * - * @category Account - * @package StatusNet - * @author Evan Prodromou - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 - * @link http://status.net/ - */ - -if (!defined('STATUSNET')) { - // This check helps protect against security problems; - // your code file can't be executed directly from the web. - exit(1); -} - -/** - * A class for restoring accounts - * - * This is a clumsy objectification of the functions in restoreuser.php. - * - * Note that it quite illegally uses the OStatus_profile class which may - * not even exist on this server. - * - * @category Account - * @package StatusNet - * @author Evan Prodromou - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 - * @link http://status.net/ - */ - -class AccountRestorer -{ - private $_trusted = false; - - function loadXML($xml) - { - $dom = DOMDocument::loadXML($xml); - - if ($dom->documentElement->namespaceURI != Activity::ATOM || - $dom->documentElement->localName != 'feed') { - throw new Exception("'$filename' is not an Atom feed."); - } - - return $dom; - } - - function importActivityStream($user, $doc) - { - $feed = $doc->documentElement; - - $subjectEl = ActivityUtils::child($feed, Activity::SUBJECT, Activity::SPEC); - - if (!empty($subjectEl)) { - $subject = new ActivityObject($subjectEl); - } else { - throw new Exception("Feed doesn't have an element."); - } - - if (is_null($user)) { - $user = $this->userFromSubject($subject); - } - - $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); - - $activities = $this->entriesToActivities($entries, $feed); - - // XXX: sort entries here - - foreach ($activities as $activity) { - try { - switch ($activity->verb) { - case ActivityVerb::FOLLOW: - $this->subscribeProfile($user, $subject, $activity); - break; - case ActivityVerb::JOIN: - $this->joinGroup($user, $activity); - break; - case ActivityVerb::POST: - $this->postNote($user, $activity); - break; - default: - throw new Exception("Unknown verb: {$activity->verb}"); - } - } catch (Exception $e) { - common_log(LOG_WARNING, $e->getMessage()); - continue; - } - } - } - - function subscribeProfile($user, $subject, $activity) - { - $profile = $user->getProfile(); - - if ($activity->objects[0]->id == $subject->id) { - if (!$this->_trusted) { - throw new Exception("Skipping a pushed subscription."); - } else { - $other = $activity->actor; - $otherUser = User::staticGet('uri', $other->id); - - if (!empty($otherUser)) { - $otherProfile = $otherUser->getProfile(); - } else { - throw new Exception("Can't force remote user to subscribe."); - } - // XXX: don't do this for untrusted input! - Subscription::start($otherProfile, $profile); - } - } else if (empty($activity->actor) - || $activity->actor->id == $subject->id) { - - $other = $activity->objects[0]; - $otherUser = User::staticGet('uri', $other->id); - - if (!empty($otherUser)) { - $otherProfile = $otherUser->getProfile(); - } else { - $oprofile = Ostatus_profile::ensureActivityObjectProfile($other); - $otherProfile = $oprofile->localProfile(); - } - - Subscription::start($profile, $otherProfile); - } else { - throw new Exception("This activity seems unrelated to our user."); - } - } - - function joinGroup($user, $activity) - { - // XXX: check that actor == subject - - $uri = $activity->objects[0]->id; - - $group = User_group::staticGet('uri', $uri); - - if (empty($group)) { - $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]); - if (!$oprofile->isGroup()) { - throw new Exception("Remote profile is not a group!"); - } - $group = $oprofile->localGroup(); - } - - assert(!empty($group)); - - if (Event::handle('StartJoinGroup', array($group, $user))) { - Group_member::join($group->id, $user->id); - Event::handle('EndJoinGroup', array($group, $user)); - } - } - - // XXX: largely cadged from Ostatus_profile::processNote() - - function postNote($user, $activity) - { - $note = $activity->objects[0]; - - $sourceUri = $note->id; - - $notice = Notice::staticGet('uri', $sourceUri); - - if (!empty($notice)) { - // This is weird. - $orig = clone($notice); - $notice->profile_id = $user->id; - $notice->update($orig); - return; - } - - // Use summary as fallback for content - - if (!empty($note->content)) { - $sourceContent = $note->content; - } else if (!empty($note->summary)) { - $sourceContent = $note->summary; - } else if (!empty($note->title)) { - $sourceContent = $note->title; - } else { - // @fixme fetch from $sourceUrl? - // @todo i18n FIXME: use sprintf and add i18n. - throw new ClientException("No content for notice {$sourceUri}."); - } - - // Get (safe!) HTML and text versions of the content - - $rendered = $this->purify($sourceContent); - $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8'); - - $shortened = $user->shortenLinks($content); - - $options = array('is_local' => Notice::LOCAL_PUBLIC, - 'uri' => $sourceUri, - 'rendered' => $rendered, - 'replies' => array(), - 'groups' => array(), - 'tags' => array(), - 'urls' => array()); - - // Check for optional attributes... - - if (!empty($activity->time)) { - $options['created'] = common_sql_date($activity->time); - } - - if ($activity->context) { - // Any individual or group attn: targets? - - list($options['groups'], $options['replies']) = $this->filterAttention($activity->context->attention); - - // Maintain direct reply associations - // @fixme what about conversation ID? - if (!empty($activity->context->replyToID)) { - $orig = Notice::staticGet('uri', - $activity->context->replyToID); - if (!empty($orig)) { - $options['reply_to'] = $orig->id; - } - } - - $location = $activity->context->location; - - if ($location) { - $options['lat'] = $location->lat; - $options['lon'] = $location->lon; - if ($location->location_id) { - $options['location_ns'] = $location->location_ns; - $options['location_id'] = $location->location_id; - } - } - } - - // Atom categories <-> hashtags - - foreach ($activity->categories as $cat) { - if ($cat->term) { - $term = common_canonical_tag($cat->term); - if ($term) { - $options['tags'][] = $term; - } - } - } - - // Atom enclosures -> attachment URLs - foreach ($activity->enclosures as $href) { - // @fixme save these locally or....? - $options['urls'][] = $href; - } - - $saved = Notice::saveNew($user->id, - $content, - 'restore', // TODO: restore the actual source - $options); - - return $saved; - } - - function filterAttention($attn) - { - $groups = array(); - $replies = array(); - - foreach (array_unique($attn) as $recipient) { - - // Is the recipient a local user? - - $user = User::staticGet('uri', $recipient); - - if ($user) { - // @fixme sender verification, spam etc? - $replies[] = $recipient; - continue; - } - - // Is the recipient a remote group? - $oprofile = Ostatus_profile::ensureProfileURI($recipient); - - if ($oprofile) { - if (!$oprofile->isGroup()) { - // may be canonicalized or something - $replies[] = $oprofile->uri; - } - continue; - } - - // Is the recipient a local group? - // @fixme uri on user_group isn't reliable yet - // $group = User_group::staticGet('uri', $recipient); - $id = OStatusPlugin::localGroupFromUrl($recipient); - - if ($id) { - $group = User_group::staticGet('id', $id); - if ($group) { - // Deliver to all members of this local group if allowed. - $profile = $sender->localProfile(); - if ($profile->isMember($group)) { - $groups[] = $group->id; - } else { - common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member"); - } - continue; - } else { - common_log(LOG_INFO, "Skipping reply to bogus group $recipient"); - } - } - } - - return array($groups, $replies); - } - - function userFromSubject($subject) - { - $user = User::staticGet('uri', $subject->id); - - if (empty($user)) { - $attrs = - array('nickname' => Ostatus_profile::getActivityObjectNickname($subject), - 'uri' => $subject->id); - - $user = User::register($attrs); - } - - $profile = $user->getProfile(); - Ostatus_profile::updateProfile($profile, $subject); - - // FIXME: Update avatar - return $user; - } - - function purify($content) - { - $config = array('safe' => 1, - 'deny_attribute' => 'id,style,on*'); - return htmLawed($content, $config); - } -} diff --git a/lib/feedimporter.php b/lib/feedimporter.php new file mode 100644 index 000000000..3f6ac0da4 --- /dev/null +++ b/lib/feedimporter.php @@ -0,0 +1,358 @@ +. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * A class for restoring accounts + * + * This is a clumsy objectification of the functions in restoreuser.php. + * + * Note that it quite illegally uses the OStatus_profile class which may + * not even exist on this server. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class AccountRestorer +{ + private $_trusted = false; + + function loadXML($xml) + { + $dom = DOMDocument::loadXML($xml); + + if ($dom->documentElement->namespaceURI != Activity::ATOM || + $dom->documentElement->localName != 'feed') { + throw new Exception("'$filename' is not an Atom feed."); + } + + return $dom; + } + + function importActivityStream($user, $doc) + { + $feed = $doc->documentElement; + + $subjectEl = ActivityUtils::child($feed, Activity::SUBJECT, Activity::SPEC); + + if (!empty($subjectEl)) { + $subject = new ActivityObject($subjectEl); + } else { + throw new Exception("Feed doesn't have an element."); + } + + if (is_null($user)) { + $user = $this->userFromSubject($subject); + } + + $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); + + $activities = $this->entriesToActivities($entries, $feed); + + // XXX: sort entries here + + foreach ($activities as $activity) { + try { + switch ($activity->verb) { + case ActivityVerb::FOLLOW: + $this->subscribeProfile($user, $subject, $activity); + break; + case ActivityVerb::JOIN: + $this->joinGroup($user, $activity); + break; + case ActivityVerb::POST: + $this->postNote($user, $activity); + break; + default: + throw new Exception("Unknown verb: {$activity->verb}"); + } + } catch (Exception $e) { + common_log(LOG_WARNING, $e->getMessage()); + continue; + } + } + } + + function subscribeProfile($user, $subject, $activity) + { + $profile = $user->getProfile(); + + if ($activity->objects[0]->id == $subject->id) { + if (!$this->_trusted) { + throw new Exception("Skipping a pushed subscription."); + } else { + $other = $activity->actor; + $otherUser = User::staticGet('uri', $other->id); + + if (!empty($otherUser)) { + $otherProfile = $otherUser->getProfile(); + } else { + throw new Exception("Can't force remote user to subscribe."); + } + // XXX: don't do this for untrusted input! + Subscription::start($otherProfile, $profile); + } + } else if (empty($activity->actor) + || $activity->actor->id == $subject->id) { + + $other = $activity->objects[0]; + $otherUser = User::staticGet('uri', $other->id); + + if (!empty($otherUser)) { + $otherProfile = $otherUser->getProfile(); + } else { + $oprofile = Ostatus_profile::ensureActivityObjectProfile($other); + $otherProfile = $oprofile->localProfile(); + } + + Subscription::start($profile, $otherProfile); + } else { + throw new Exception("This activity seems unrelated to our user."); + } + } + + function joinGroup($user, $activity) + { + // XXX: check that actor == subject + + $uri = $activity->objects[0]->id; + + $group = User_group::staticGet('uri', $uri); + + if (empty($group)) { + $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]); + if (!$oprofile->isGroup()) { + throw new Exception("Remote profile is not a group!"); + } + $group = $oprofile->localGroup(); + } + + assert(!empty($group)); + + if (Event::handle('StartJoinGroup', array($group, $user))) { + Group_member::join($group->id, $user->id); + Event::handle('EndJoinGroup', array($group, $user)); + } + } + + // XXX: largely cadged from Ostatus_profile::processNote() + + function postNote($user, $activity) + { + $note = $activity->objects[0]; + + $sourceUri = $note->id; + + $notice = Notice::staticGet('uri', $sourceUri); + + if (!empty($notice)) { + // This is weird. + $orig = clone($notice); + $notice->profile_id = $user->id; + $notice->update($orig); + return; + } + + // Use summary as fallback for content + + if (!empty($note->content)) { + $sourceContent = $note->content; + } else if (!empty($note->summary)) { + $sourceContent = $note->summary; + } else if (!empty($note->title)) { + $sourceContent = $note->title; + } else { + // @fixme fetch from $sourceUrl? + // @todo i18n FIXME: use sprintf and add i18n. + throw new ClientException("No content for notice {$sourceUri}."); + } + + // Get (safe!) HTML and text versions of the content + + $rendered = $this->purify($sourceContent); + $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8'); + + $shortened = $user->shortenLinks($content); + + $options = array('is_local' => Notice::LOCAL_PUBLIC, + 'uri' => $sourceUri, + 'rendered' => $rendered, + 'replies' => array(), + 'groups' => array(), + 'tags' => array(), + 'urls' => array()); + + // Check for optional attributes... + + if (!empty($activity->time)) { + $options['created'] = common_sql_date($activity->time); + } + + if ($activity->context) { + // Any individual or group attn: targets? + + list($options['groups'], $options['replies']) = $this->filterAttention($activity->context->attention); + + // Maintain direct reply associations + // @fixme what about conversation ID? + if (!empty($activity->context->replyToID)) { + $orig = Notice::staticGet('uri', + $activity->context->replyToID); + if (!empty($orig)) { + $options['reply_to'] = $orig->id; + } + } + + $location = $activity->context->location; + + if ($location) { + $options['lat'] = $location->lat; + $options['lon'] = $location->lon; + if ($location->location_id) { + $options['location_ns'] = $location->location_ns; + $options['location_id'] = $location->location_id; + } + } + } + + // Atom categories <-> hashtags + + foreach ($activity->categories as $cat) { + if ($cat->term) { + $term = common_canonical_tag($cat->term); + if ($term) { + $options['tags'][] = $term; + } + } + } + + // Atom enclosures -> attachment URLs + foreach ($activity->enclosures as $href) { + // @fixme save these locally or....? + $options['urls'][] = $href; + } + + $saved = Notice::saveNew($user->id, + $content, + 'restore', // TODO: restore the actual source + $options); + + return $saved; + } + + function filterAttention($attn) + { + $groups = array(); + $replies = array(); + + foreach (array_unique($attn) as $recipient) { + + // Is the recipient a local user? + + $user = User::staticGet('uri', $recipient); + + if ($user) { + // @fixme sender verification, spam etc? + $replies[] = $recipient; + continue; + } + + // Is the recipient a remote group? + $oprofile = Ostatus_profile::ensureProfileURI($recipient); + + if ($oprofile) { + if (!$oprofile->isGroup()) { + // may be canonicalized or something + $replies[] = $oprofile->uri; + } + continue; + } + + // Is the recipient a local group? + // @fixme uri on user_group isn't reliable yet + // $group = User_group::staticGet('uri', $recipient); + $id = OStatusPlugin::localGroupFromUrl($recipient); + + if ($id) { + $group = User_group::staticGet('id', $id); + if ($group) { + // Deliver to all members of this local group if allowed. + $profile = $sender->localProfile(); + if ($profile->isMember($group)) { + $groups[] = $group->id; + } else { + common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member"); + } + continue; + } else { + common_log(LOG_INFO, "Skipping reply to bogus group $recipient"); + } + } + } + + return array($groups, $replies); + } + + function userFromSubject($subject) + { + $user = User::staticGet('uri', $subject->id); + + if (empty($user)) { + $attrs = + array('nickname' => Ostatus_profile::getActivityObjectNickname($subject), + 'uri' => $subject->id); + + $user = User::register($attrs); + } + + $profile = $user->getProfile(); + Ostatus_profile::updateProfile($profile, $subject); + + // FIXME: Update avatar + return $user; + } + + function purify($content) + { + $config = array('safe' => 1, + 'deny_attribute' => 'id,style,on*'); + return htmLawed($content, $config); + } +} -- cgit v1.2.3 From 044763cf06b1fb99cd8246dcbb8bb4a3e545d3ed Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 17 Dec 2010 13:12:17 -0500 Subject: move activity importing code to two different queuehandler classes --- lib/activityimporter.php | 317 ++++++++++++++++++++++++++++++++++++++++++++++ lib/feedimporter.php | 321 +++++++---------------------------------------- lib/queuemanager.php | 2 + 3 files changed, 365 insertions(+), 275 deletions(-) create mode 100644 lib/activityimporter.php diff --git a/lib/activityimporter.php b/lib/activityimporter.php new file mode 100644 index 000000000..07a6b0e77 --- /dev/null +++ b/lib/activityimporter.php @@ -0,0 +1,317 @@ +. + * + * @category Cache + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Class comment + * + * @category General + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class ActivityImporter extends QueueHandler +{ + private $trusted = false; + + /** + * Function comment + * + * @param + * + * @return + */ + + function handle($data) + { + list($user, $author, $activity, $trusted) = $data; + + $this->trusted = $trusted; + + try { + switch ($activity->verb) { + case ActivityVerb::FOLLOW: + $this->subscribeProfile($user, $author, $activity); + break; + case ActivityVerb::JOIN: + $this->joinGroup($user, $activity); + break; + case ActivityVerb::POST: + $this->postNote($user, $activity); + break; + default: + throw new Exception("Unknown verb: {$activity->verb}"); + } + } catch (ClientException $ce) { + common_log(LOG_WARNING, $ce->getMessage()); + return true; + } catch (ServerException $se) { + common_log(LOG_ERR, $ce->getMessage()); + return false; + } catch (Exception $e) { + common_log(LOG_ERR, $ce->getMessage()); + return false; + } + return true; + } + + function subscribeProfile($user, $author, $activity) + { + $profile = $user->getProfile(); + + if ($activity->objects[0]->id == $author->id) { + $other = $activity->actor; + $otherUser = User::staticGet('uri', $other->id); + + if (!empty($otherUser)) { + $otherProfile = $otherUser->getProfile(); + } else { + throw new Exception("Can't force remote user to subscribe."); + } + + // XXX: don't do this for untrusted input! + + Subscription::start($otherProfile, $profile); + + } else if (empty($activity->actor) + || $activity->actor->id == $author->id) { + + $other = $activity->objects[0]; + + $otherProfile = Profile::fromUri($other->id); + + if (empty($otherProfile)) { + throw new ClientException(_("Unknown profile.")); + } + + Subscription::start($profile, $otherProfile); + } else { + throw new Exception("This activity seems unrelated to our user."); + } + } + + function joinGroup($user, $activity) + { + // XXX: check that actor == subject + + $uri = $activity->objects[0]->id; + + $group = User_group::staticGet('uri', $uri); + + if (empty($group)) { + $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]); + if (!$oprofile->isGroup()) { + throw new Exception("Remote profile is not a group!"); + } + $group = $oprofile->localGroup(); + } + + assert(!empty($group)); + + if (Event::handle('StartJoinGroup', array($group, $user))) { + Group_member::join($group->id, $user->id); + Event::handle('EndJoinGroup', array($group, $user)); + } + } + + // XXX: largely cadged from Ostatus_profile::processNote() + + function postNote($user, $activity) + { + $note = $activity->objects[0]; + + $sourceUri = $note->id; + + $notice = Notice::staticGet('uri', $sourceUri); + + if (!empty($notice)) { + // This is weird. + $orig = clone($notice); + $notice->profile_id = $user->id; + $notice->update($orig); + return; + } + + // Use summary as fallback for content + + if (!empty($note->content)) { + $sourceContent = $note->content; + } else if (!empty($note->summary)) { + $sourceContent = $note->summary; + } else if (!empty($note->title)) { + $sourceContent = $note->title; + } else { + // @fixme fetch from $sourceUrl? + // @todo i18n FIXME: use sprintf and add i18n. + throw new ClientException("No content for notice {$sourceUri}."); + } + + // Get (safe!) HTML and text versions of the content + + $rendered = $this->purify($sourceContent); + $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8'); + + $shortened = $user->shortenLinks($content); + + $options = array('is_local' => Notice::LOCAL_PUBLIC, + 'uri' => $sourceUri, + 'rendered' => $rendered, + 'replies' => array(), + 'groups' => array(), + 'tags' => array(), + 'urls' => array()); + + // Check for optional attributes... + + if (!empty($activity->time)) { + $options['created'] = common_sql_date($activity->time); + } + + if ($activity->context) { + // Any individual or group attn: targets? + + list($options['groups'], $options['replies']) = $this->filterAttention($activity->context->attention); + + // Maintain direct reply associations + // @fixme what about conversation ID? + if (!empty($activity->context->replyToID)) { + $orig = Notice::staticGet('uri', + $activity->context->replyToID); + if (!empty($orig)) { + $options['reply_to'] = $orig->id; + } + } + + $location = $activity->context->location; + + if ($location) { + $options['lat'] = $location->lat; + $options['lon'] = $location->lon; + if ($location->location_id) { + $options['location_ns'] = $location->location_ns; + $options['location_id'] = $location->location_id; + } + } + } + + // Atom categories <-> hashtags + + foreach ($activity->categories as $cat) { + if ($cat->term) { + $term = common_canonical_tag($cat->term); + if ($term) { + $options['tags'][] = $term; + } + } + } + + // Atom enclosures -> attachment URLs + foreach ($activity->enclosures as $href) { + // @fixme save these locally or....? + $options['urls'][] = $href; + } + + $saved = Notice::saveNew($user->id, + $content, + 'restore', // TODO: restore the actual source + $options); + + return $saved; + } + + function filterAttention($attn) + { + $groups = array(); + $replies = array(); + + foreach (array_unique($attn) as $recipient) { + + // Is the recipient a local user? + + $user = User::staticGet('uri', $recipient); + + if ($user) { + // @fixme sender verification, spam etc? + $replies[] = $recipient; + continue; + } + + // Is the recipient a remote group? + $oprofile = Ostatus_profile::ensureProfileURI($recipient); + + if ($oprofile) { + if (!$oprofile->isGroup()) { + // may be canonicalized or something + $replies[] = $oprofile->uri; + } + continue; + } + + // Is the recipient a local group? + // @fixme uri on user_group isn't reliable yet + // $group = User_group::staticGet('uri', $recipient); + $id = OStatusPlugin::localGroupFromUrl($recipient); + + if ($id) { + $group = User_group::staticGet('id', $id); + if ($group) { + // Deliver to all members of this local group if allowed. + $profile = $sender->localProfile(); + if ($profile->isMember($group)) { + $groups[] = $group->id; + } else { + common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member"); + } + continue; + } else { + common_log(LOG_INFO, "Skipping reply to bogus group $recipient"); + } + } + } + + return array($groups, $replies); + } + + + function purify($content) + { + $config = array('safe' => 1, + 'deny_attribute' => 'id,style,on*'); + return htmLawed($content, $config); + } +} diff --git a/lib/feedimporter.php b/lib/feedimporter.php index 3f6ac0da4..e2c9df72f 100644 --- a/lib/feedimporter.php +++ b/lib/feedimporter.php @@ -3,7 +3,7 @@ * StatusNet - the distributed open-source microblogging tool * Copyright (C) 2010, StatusNet, Inc. * - * A class for restoring accounts + * Importer for feeds of activities * * PHP version 5 * @@ -35,13 +35,11 @@ if (!defined('STATUSNET')) { } /** - * A class for restoring accounts + * Importer for feeds of activities + * + * Takes an XML file representing a feed of activities and imports each + * activity to the user in question. * - * This is a clumsy objectification of the functions in restoreuser.php. - * - * Note that it quite illegally uses the OStatus_profile class which may - * not even exist on this server. - * * @category Account * @package StatusNet * @author Evan Prodromou @@ -50,309 +48,82 @@ if (!defined('STATUSNET')) { * @link http://status.net/ */ -class AccountRestorer +class FeedImporter extends QueueHandler { - private $_trusted = false; + /** + * Transport identifier + * + * @return string identifier for this queue handler + */ - function loadXML($xml) + public function transport() { - $dom = DOMDocument::loadXML($xml); - - if ($dom->documentElement->namespaceURI != Activity::ATOM || - $dom->documentElement->localName != 'feed') { - throw new Exception("'$filename' is not an Atom feed."); - } - - return $dom; - } - - function importActivityStream($user, $doc) - { - $feed = $doc->documentElement; - - $subjectEl = ActivityUtils::child($feed, Activity::SUBJECT, Activity::SPEC); - - if (!empty($subjectEl)) { - $subject = new ActivityObject($subjectEl); - } else { - throw new Exception("Feed doesn't have an element."); - } - - if (is_null($user)) { - $user = $this->userFromSubject($subject); - } - - $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); - - $activities = $this->entriesToActivities($entries, $feed); - - // XXX: sort entries here - - foreach ($activities as $activity) { - try { - switch ($activity->verb) { - case ActivityVerb::FOLLOW: - $this->subscribeProfile($user, $subject, $activity); - break; - case ActivityVerb::JOIN: - $this->joinGroup($user, $activity); - break; - case ActivityVerb::POST: - $this->postNote($user, $activity); - break; - default: - throw new Exception("Unknown verb: {$activity->verb}"); - } - } catch (Exception $e) { - common_log(LOG_WARNING, $e->getMessage()); - continue; - } - } + return 'feedimp'; } - function subscribeProfile($user, $subject, $activity) + function handle($data) { - $profile = $user->getProfile(); + list($user, $xml, $trusted) = $data; - if ($activity->objects[0]->id == $subject->id) { - if (!$this->_trusted) { - throw new Exception("Skipping a pushed subscription."); - } else { - $other = $activity->actor; - $otherUser = User::staticGet('uri', $other->id); + try { + $doc = DOMDocument::loadXML($xml); - if (!empty($otherUser)) { - $otherProfile = $otherUser->getProfile(); - } else { - throw new Exception("Can't force remote user to subscribe."); - } - // XXX: don't do this for untrusted input! - Subscription::start($otherProfile, $profile); + if ($doc->documentElement->namespaceURI != Activity::ATOM || + $doc->documentElement->localName != 'feed') { + throw new ClientException(_("Not an atom feed.")); } - } else if (empty($activity->actor) - || $activity->actor->id == $subject->id) { - - $other = $activity->objects[0]; - $otherUser = User::staticGet('uri', $other->id); - if (!empty($otherUser)) { - $otherProfile = $otherUser->getProfile(); - } else { - $oprofile = Ostatus_profile::ensureActivityObjectProfile($other); - $otherProfile = $oprofile->localProfile(); - } + $feed = $doc->documentElement; - Subscription::start($profile, $otherProfile); - } else { - throw new Exception("This activity seems unrelated to our user."); - } - } + $author = ActivityUtils::getFeedAuthor($feed); - function joinGroup($user, $activity) - { - // XXX: check that actor == subject - - $uri = $activity->objects[0]->id; - - $group = User_group::staticGet('uri', $uri); - - if (empty($group)) { - $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]); - if (!$oprofile->isGroup()) { - throw new Exception("Remote profile is not a group!"); + if (empty($author)) { + throw new ClientException(_("No author in the feed.")); } - $group = $oprofile->localGroup(); - } - assert(!empty($group)); - - if (Event::handle('StartJoinGroup', array($group, $user))) { - Group_member::join($group->id, $user->id); - Event::handle('EndJoinGroup', array($group, $user)); - } - } - - // XXX: largely cadged from Ostatus_profile::processNote() - - function postNote($user, $activity) - { - $note = $activity->objects[0]; - - $sourceUri = $note->id; - - $notice = Notice::staticGet('uri', $sourceUri); - - if (!empty($notice)) { - // This is weird. - $orig = clone($notice); - $notice->profile_id = $user->id; - $notice->update($orig); - return; - } - - // Use summary as fallback for content - - if (!empty($note->content)) { - $sourceContent = $note->content; - } else if (!empty($note->summary)) { - $sourceContent = $note->summary; - } else if (!empty($note->title)) { - $sourceContent = $note->title; - } else { - // @fixme fetch from $sourceUrl? - // @todo i18n FIXME: use sprintf and add i18n. - throw new ClientException("No content for notice {$sourceUri}."); - } - - // Get (safe!) HTML and text versions of the content - - $rendered = $this->purify($sourceContent); - $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8'); - - $shortened = $user->shortenLinks($content); - - $options = array('is_local' => Notice::LOCAL_PUBLIC, - 'uri' => $sourceUri, - 'rendered' => $rendered, - 'replies' => array(), - 'groups' => array(), - 'tags' => array(), - 'urls' => array()); - - // Check for optional attributes... - - if (!empty($activity->time)) { - $options['created'] = common_sql_date($activity->time); - } - - if ($activity->context) { - // Any individual or group attn: targets? - - list($options['groups'], $options['replies']) = $this->filterAttention($activity->context->attention); - - // Maintain direct reply associations - // @fixme what about conversation ID? - if (!empty($activity->context->replyToID)) { - $orig = Notice::staticGet('uri', - $activity->context->replyToID); - if (!empty($orig)) { - $options['reply_to'] = $orig->id; + if (empty($user)) { + if ($trusted) { + $user = $this->userFromAuthor($author); } } - $location = $activity->context->location; + $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); - if ($location) { - $options['lat'] = $location->lat; - $options['lon'] = $location->lon; - if ($location->location_id) { - $options['location_ns'] = $location->location_ns; - $options['location_id'] = $location->location_id; - } - } - } + $activities = $this->entriesToActivities($entries, $feed); - // Atom categories <-> hashtags + $qm = QueueManager::get(); - foreach ($activity->categories as $cat) { - if ($cat->term) { - $term = common_canonical_tag($cat->term); - if ($term) { - $options['tags'][] = $term; - } + foreach ($activities as $activity) { + $qm->enqueue(array($user, $author, $activity, $trusted), 'actimp'); } + } catch (ClientException $ce) { + common_log(LOG_WARNING, $ce->getMessage()); + return true; + } catch (ServerException $se) { + common_log(LOG_ERR, $ce->getMessage()); + return false; + } catch (Exception $e) { + common_log(LOG_ERR, $ce->getMessage()); + return false; } - - // Atom enclosures -> attachment URLs - foreach ($activity->enclosures as $href) { - // @fixme save these locally or....? - $options['urls'][] = $href; - } - - $saved = Notice::saveNew($user->id, - $content, - 'restore', // TODO: restore the actual source - $options); - - return $saved; } - function filterAttention($attn) + function userFromAuthor($author) { - $groups = array(); - $replies = array(); - - foreach (array_unique($attn) as $recipient) { - - // Is the recipient a local user? - - $user = User::staticGet('uri', $recipient); - - if ($user) { - // @fixme sender verification, spam etc? - $replies[] = $recipient; - continue; - } - - // Is the recipient a remote group? - $oprofile = Ostatus_profile::ensureProfileURI($recipient); - - if ($oprofile) { - if (!$oprofile->isGroup()) { - // may be canonicalized or something - $replies[] = $oprofile->uri; - } - continue; - } - - // Is the recipient a local group? - // @fixme uri on user_group isn't reliable yet - // $group = User_group::staticGet('uri', $recipient); - $id = OStatusPlugin::localGroupFromUrl($recipient); - - if ($id) { - $group = User_group::staticGet('id', $id); - if ($group) { - // Deliver to all members of this local group if allowed. - $profile = $sender->localProfile(); - if ($profile->isMember($group)) { - $groups[] = $group->id; - } else { - common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member"); - } - continue; - } else { - common_log(LOG_INFO, "Skipping reply to bogus group $recipient"); - } - } - } - - return array($groups, $replies); - } - - function userFromSubject($subject) - { - $user = User::staticGet('uri', $subject->id); + $user = User::staticGet('uri', $author->id); if (empty($user)) { $attrs = - array('nickname' => Ostatus_profile::getActivityObjectNickname($subject), - 'uri' => $subject->id); + array('nickname' => Ostatus_profile::getActivityObjectNickname($author), + 'uri' => $author->id); $user = User::register($attrs); } $profile = $user->getProfile(); - Ostatus_profile::updateProfile($profile, $subject); + Ostatus_profile::updateProfile($profile, $author); // FIXME: Update avatar return $user; } - - function purify($content) - { - $config = array('safe' => 1, - 'deny_attribute' => 'id,style,on*'); - return htmLawed($content, $config); - } } diff --git a/lib/queuemanager.php b/lib/queuemanager.php index 0829c8a8b..65a972e23 100644 --- a/lib/queuemanager.php +++ b/lib/queuemanager.php @@ -266,6 +266,8 @@ abstract class QueueManager extends IoManager // Background user management tasks... $this->connect('deluser', 'DelUserQueueHandler'); + $this->connect('feedimp', 'FeedImporter'); + $this->connect('actimp', 'ActivityImporter'); // Broadcasting profile updates to OMB remote subscribers $this->connect('profile', 'ProfileQueueHandler'); -- cgit v1.2.3 From 4b41d05a13cc8d49871687b767640fbcd15eb05c Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 17 Dec 2010 16:27:20 -0500 Subject: Make restoreuser use new FeedImporter queue handler --- lib/activityimporter.php | 9 +++++++-- lib/feedimporter.php | 36 +++++++++++++++++++++++++++++++++--- scripts/restoreuser.php | 5 ++--- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/lib/activityimporter.php b/lib/activityimporter.php index 07a6b0e77..e936449c8 100644 --- a/lib/activityimporter.php +++ b/lib/activityimporter.php @@ -81,10 +81,10 @@ class ActivityImporter extends QueueHandler common_log(LOG_WARNING, $ce->getMessage()); return true; } catch (ServerException $se) { - common_log(LOG_ERR, $ce->getMessage()); + common_log(LOG_ERR, $se->getMessage()); return false; } catch (Exception $e) { - common_log(LOG_ERR, $ce->getMessage()); + common_log(LOG_ERR, $e->getMessage()); return false; } return true; @@ -95,6 +95,11 @@ class ActivityImporter extends QueueHandler $profile = $user->getProfile(); if ($activity->objects[0]->id == $author->id) { + + if (!$this->trusted) { + throw new ClientException(_("Can't force subscription for untrusted user.")); + } + $other = $activity->actor; $otherUser = User::staticGet('uri', $other->id); diff --git a/lib/feedimporter.php b/lib/feedimporter.php index e2c9df72f..0b94eeb9b 100644 --- a/lib/feedimporter.php +++ b/lib/feedimporter.php @@ -84,12 +84,12 @@ class FeedImporter extends QueueHandler if (empty($user)) { if ($trusted) { $user = $this->userFromAuthor($author); + } else { + throw new ClientException(_("Can't import without a user.")); } } - $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); - - $activities = $this->entriesToActivities($entries, $feed); + $activities = $this->getActivities($feed); $qm = QueueManager::get(); @@ -108,6 +108,36 @@ class FeedImporter extends QueueHandler } } + function getActivities($feed) + { + $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry'); + + $activities = array(); + + for ($i = 0; $i < $entries->length; $i++) { + $activities[] = new Activity($entries->item($i)); + } + + usort($activities, array("FeedImporter", "activitySort")); + + return $activities; + } + + /** + * Sort activities oldest-first + */ + + static function activitySort($a, $b) + { + if ($a->time == $b->time) { + return 0; + } else if ($a->time < $b->time) { + return -1; + } else { + return 1; + } + } + function userFromAuthor($author) { $user = User::staticGet('uri', $author->id); diff --git a/scripts/restoreuser.php b/scripts/restoreuser.php index eac7e5cf2..17f007b41 100644 --- a/scripts/restoreuser.php +++ b/scripts/restoreuser.php @@ -75,9 +75,8 @@ try { $user = null; } $xml = getActivityStreamDocument(); - $restorer = new AccountRestorer(); - $doc = $restorer->loadXML($xml); - $restorer->importActivityStream($user, $doc); + $qm = QueueManager::get(); + $qm->enqueue(array($user, $xml, true), 'feedimp'); } catch (Exception $e) { print $e->getMessage()."\n"; exit(1); -- cgit v1.2.3 From 1a81356622dba7bcbf75de6eb10f28170e972b96 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 17 Dec 2010 17:37:43 -0500 Subject: I'm still not sure when it's useful to reset a notice's author --- lib/activityimporter.php | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/activityimporter.php b/lib/activityimporter.php index e936449c8..28c371e4d 100644 --- a/lib/activityimporter.php +++ b/lib/activityimporter.php @@ -72,7 +72,7 @@ class ActivityImporter extends QueueHandler $this->joinGroup($user, $activity); break; case ActivityVerb::POST: - $this->postNote($user, $activity); + $this->postNote($user, $author, $activity); break; default: throw new Exception("Unknown verb: {$activity->verb}"); @@ -141,13 +141,17 @@ class ActivityImporter extends QueueHandler if (empty($group)) { $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]); if (!$oprofile->isGroup()) { - throw new Exception("Remote profile is not a group!"); + throw new ClientException("Remote profile is not a group!"); } $group = $oprofile->localGroup(); } assert(!empty($group)); + if ($user->isMember($group)) { + throw new ClientException("User is already a member of this group."); + } + if (Event::handle('StartJoinGroup', array($group, $user))) { Group_member::join($group->id, $user->id); Event::handle('EndJoinGroup', array($group, $user)); @@ -156,7 +160,7 @@ class ActivityImporter extends QueueHandler // XXX: largely cadged from Ostatus_profile::processNote() - function postNote($user, $activity) + function postNote($user, $author, $activity) { $note = $activity->objects[0]; @@ -165,11 +169,27 @@ class ActivityImporter extends QueueHandler $notice = Notice::staticGet('uri', $sourceUri); if (!empty($notice)) { - // This is weird. - $orig = clone($notice); - $notice->profile_id = $user->id; - $notice->update($orig); - return; + + common_log(LOG_INFO, "Notice {$sourceUri} already exists."); + + if ($this->trusted) { + + $profile = $notice->getProfile(); + + $uri = $profile->getUri(); + + if ($uri == $author->id) { + common_log(LOG_INFO, "Updating notice author from $author->id to $user->uri"); + $orig = clone($notice); + $notice->profile_id = $user->id; + $notice->update($orig); + return; + } else { + throw new ClientException(sprintf(_("Already know about notice %s and ". + " it's got a different author %s."), + $sourceUri, $uri)); + } + } } // Use summary as fallback for content @@ -199,7 +219,8 @@ class ActivityImporter extends QueueHandler 'replies' => array(), 'groups' => array(), 'tags' => array(), - 'urls' => array()); + 'urls' => array(), + 'distribute' => false); // Check for optional attributes... @@ -251,6 +272,8 @@ class ActivityImporter extends QueueHandler $options['urls'][] = $href; } + common_log(LOG_INFO, "Saving notice {$options['uri']}"); + $saved = Notice::saveNew($user->id, $content, 'restore', // TODO: restore the actual source -- cgit v1.2.3 From 120802b807145db6419f964f3c5fecf49ea7411d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 17 Dec 2010 18:55:00 -0500 Subject: change code order to make shorter lines --- lib/feedimporter.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/feedimporter.php b/lib/feedimporter.php index 0b94eeb9b..e46858cc5 100644 --- a/lib/feedimporter.php +++ b/lib/feedimporter.php @@ -68,12 +68,13 @@ class FeedImporter extends QueueHandler try { $doc = DOMDocument::loadXML($xml); - if ($doc->documentElement->namespaceURI != Activity::ATOM || - $doc->documentElement->localName != 'feed') { + $feed = $doc->documentElement; + + if ($feed->namespaceURI != Activity::ATOM || + $feed->localName != 'feed') { throw new ClientException(_("Not an atom feed.")); } - $feed = $doc->documentElement; $author = ActivityUtils::getFeedAuthor($feed); -- cgit v1.2.3 From 1d6091cad20c0d5a7a31263032431ac13854a5b8 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 17 Dec 2010 18:56:17 -0500 Subject: Two bug fixes in activityimporter --- lib/activityimporter.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/activityimporter.php b/lib/activityimporter.php index 28c371e4d..4a7678132 100644 --- a/lib/activityimporter.php +++ b/lib/activityimporter.php @@ -189,6 +189,8 @@ class ActivityImporter extends QueueHandler " it's got a different author %s."), $sourceUri, $uri)); } + } else { + throw new ClientException("Not overwriting author info for non-trusted user."); } } @@ -338,8 +340,11 @@ class ActivityImporter extends QueueHandler function purify($content) { + require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php'; + $config = array('safe' => 1, 'deny_attribute' => 'id,style,on*'); + return htmLawed($content, $config); } } -- cgit v1.2.3 From 573bbeced10f06951db8875db8b4f9f0d0deca41 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 17 Dec 2010 18:56:48 -0500 Subject: action to restore a user's backup from the Web interface --- actions/profilesettings.php | 7 + actions/restoreaccount.php | 359 ++++++++++++++++++++++++++++++++++++++++++++ lib/router.php | 1 + 3 files changed, 367 insertions(+) create mode 100644 actions/restoreaccount.php diff --git a/actions/profilesettings.php b/actions/profilesettings.php index 0226e1dd4..8f55a4718 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -472,6 +472,13 @@ class ProfilesettingsAction extends AccountSettingsAction _('Delete account')); $this->elementEnd('li'); } + if ($user->hasRight(Right::RESTOREACCOUNT)) { + $this->elementStart('li'); + $this->element('a', + array('href' => common_local_url('restoreaccount')), + _('Restore account')); + $this->elementEnd('li'); + } $this->elementEnd('div'); } } diff --git a/actions/restoreaccount.php b/actions/restoreaccount.php new file mode 100644 index 000000000..c33756d48 --- /dev/null +++ b/actions/restoreaccount.php @@ -0,0 +1,359 @@ +. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Restore a backup of your own account from the browser + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class RestoreaccountAction extends Action +{ + private $success = false; + + /** + * Returns the title of the page + * + * @return string page title + */ + + function title() + { + return _("Restore account"); + } + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + $cur = common_current_user(); + + if (empty($cur)) { + throw new ClientException(_('Only logged-in users can restore their account.'), 403); + } + + if (!$cur->hasRight(Right::RESTOREACCOUNT)) { + throw new ClientException(_('You may not restore your account.'), 403); + } + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + + function handle($argarray=null) + { + parent::handle($args); + + if ($this->isPost()) { + $this->restoreAccount(); + } else { + $this->showPage(); + } + return; + } + + /** + * Queue a file for restoration + * + * Uses the UserActivityStream class; may take a long time! + * + * @return void + */ + + function restoreAccount() + { + $this->checkSessionToken(); + + if (!isset($_FILES['restorefile']['error'])) { + throw new ClientException(_('No uploaded file.')); + } + + switch ($_FILES['restorefile']['error']) { + case UPLOAD_ERR_OK: // success, jump out + break; + case UPLOAD_ERR_INI_SIZE: + // TRANS: Client exception thrown when an uploaded file is larger than set in php.ini. + throw new ClientException(_('The uploaded file exceeds the ' . + 'upload_max_filesize directive in php.ini.')); + return; + case UPLOAD_ERR_FORM_SIZE: + throw new ClientException( + // TRANS: Client exception. + _('The uploaded file exceeds the MAX_FILE_SIZE directive' . + ' that was specified in the HTML form.')); + return; + case UPLOAD_ERR_PARTIAL: + @unlink($_FILES['restorefile']['tmp_name']); + // TRANS: Client exception. + throw new ClientException(_('The uploaded file was only' . + ' partially uploaded.')); + return; + case UPLOAD_ERR_NO_FILE: + // No file; probably just a non-AJAX submission. + return; + case UPLOAD_ERR_NO_TMP_DIR: + // TRANS: Client exception thrown when a temporary folder is not present to store a file upload. + throw new ClientException(_('Missing a temporary folder.')); + return; + case UPLOAD_ERR_CANT_WRITE: + // TRANS: Client exception thrown when writing to disk is not possible during a file upload operation. + throw new ClientException(_('Failed to write file to disk.')); + return; + case UPLOAD_ERR_EXTENSION: + // TRANS: Client exception thrown when a file upload operation has been stopped by an extension. + throw new ClientException(_('File upload stopped by extension.')); + return; + default: + common_log(LOG_ERR, __METHOD__ . ": Unknown upload error " . + $_FILES['restorefile']['error']); + // TRANS: Client exception thrown when a file upload operation has failed with an unknown reason. + throw new ClientException(_('System error uploading file.')); + return; + } + + $filename = $_FILES['restorefile']['tmp_name']; + + try { + if (!file_exists($filename)) { + throw new ServerException("No such file '$filename'."); + } + + if (!is_file($filename)) { + throw new ServerException("Not a regular file: '$filename'."); + } + + if (!is_readable($filename)) { + throw new ServerException("File '$filename' not readable."); + } + + common_debug(sprintf(_("Getting backup from file '%s'."), $filename)); + + $xml = file_get_contents($filename); + + // This check is costly but we should probably give + // the user some info ahead of time. + + $doc = DOMDocument::loadXML($xml); + + $feed = $doc->documentElement; + + if ($feed->namespaceURI != Activity::ATOM || + $feed->localName != 'feed') { + throw new ClientException(_("Not an atom feed.")); + } + + // Enqueue for processing. + + $qm = QueueManager::get(); + $qm->enqueue(array(common_current_user(), $xml, false), 'feedimp'); + + $this->success = true; + + $this->showPage(); + + } catch (Exception $e) { + // Delete the file and re-throw + @unlink($_FILES['restorefile']['tmp_name']); + throw $e; + } + } + + /** + * Show a little form so that the person can upload a file to restore + * + * @return void + */ + + function showContent() + { + if ($this->success) { + $this->element('p', null, + _('Feed will be restored. Please wait a few minutes for results.')); + } else { + $form = new RestoreAccountForm($this); + $form->show(); + } + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + return false; + } + + /** + * Return last modified, if applicable. + * + * MAY override + * + * @return string last modified http header + */ + + function lastModified() + { + // For comparison with If-Last-Modified + // If not applicable, return null + return null; + } + + /** + * Return etag, if applicable. + * + * MAY override + * + * @return string etag http header + */ + + function etag() + { + return null; + } +} + +/** + * A form for backing up the account. + * + * @category Account + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class RestoreAccountForm extends Form +{ + function __construct($out=null) { + parent::__construct($out); + $this->enctype = 'multipart/form-data'; + } + + /** + * Class of the form. + * + * @return string the form's class + */ + + function formClass() + { + return 'form_profile_restore'; + } + + /** + * URL the form posts to + * + * @return string the form's action URL + */ + + function action() + { + return common_local_url('restoreaccount'); + } + + /** + * Output form data + * + * Really, just instructions for doing a backup. + * + * @return void + */ + + function formData() + { + $this->out->elementStart('p', 'instructions'); + + $this->out->raw(_('You can upload a backed-up stream in '. + 'Activity Streams format.')); + + $this->out->elementEnd('p'); + + $this->out->elementStart('ul', 'form_data'); + + $this->out->elementStart('li', array ('id' => 'settings_attach')); + $this->out->element('input', array('name' => 'restorefile', + 'type' => 'file', + 'id' => 'restorefile')); + $this->out->elementEnd('li'); + + $this->out->elementEnd('ul'); + } + + /** + * Buttons for the form + * + * In this case, a single submit button + * + * @return void + */ + + function formActions() + { + $this->out->submit('submit', + _m('BUTTON', 'Upload'), + 'submit', + null, + _('Upload the file')); + } +} diff --git a/lib/router.php b/lib/router.php index 90bc9fa35..49a16dffe 100644 --- a/lib/router.php +++ b/lib/router.php @@ -201,6 +201,7 @@ class Router 'version', 'backupaccount', 'deleteaccount', + 'restoreaccount', ); foreach ($main as $a) { -- cgit v1.2.3 From 5abd2b7d0c5565ee842969cd5205c9b428efbb6c Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 22 Dec 2010 11:06:45 -0800 Subject: fix notice error --- actions/backupaccount.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/backupaccount.php b/actions/backupaccount.php index 9454741f0..4f6fb936b 100644 --- a/actions/backupaccount.php +++ b/actions/backupaccount.php @@ -97,7 +97,7 @@ class BackupaccountAction extends Action function handle($argarray=null) { - parent::handle($args); + parent::handle($argarray); if ($this->isPost()) { $this->sendFeed(); -- cgit v1.2.3 From 754bc1b6164b82109acd14d70b295291f40d07c7 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 22 Dec 2010 11:13:57 -0800 Subject: Error handling cleanup on backup/restore: * avoid PHP notice from using wrong variable * show a visible error instead of blank screen if no file submitted with restore form * avoid PHP strict warning from using calling "non-static" DOMDocument::loadXML statically * suppress PHP warning from XML parse errors --- actions/restoreaccount.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/actions/restoreaccount.php b/actions/restoreaccount.php index c33756d48..5c8e4a12c 100644 --- a/actions/restoreaccount.php +++ b/actions/restoreaccount.php @@ -95,7 +95,7 @@ class RestoreaccountAction extends Action function handle($argarray=null) { - parent::handle($args); + parent::handle($argarray); if ($this->isPost()) { $this->restoreAccount(); @@ -143,6 +143,7 @@ class RestoreaccountAction extends Action return; case UPLOAD_ERR_NO_FILE: // No file; probably just a non-AJAX submission. + throw new ClientException(_('No uploaded file.')); return; case UPLOAD_ERR_NO_TMP_DIR: // TRANS: Client exception thrown when a temporary folder is not present to store a file upload. @@ -185,12 +186,19 @@ class RestoreaccountAction extends Action // This check is costly but we should probably give // the user some info ahead of time. + $doc = new DOMDocument(); - $doc = DOMDocument::loadXML($xml); + // Disable PHP warnings so we don't spew low-level XML errors to output... + // would be nice if we can just get exceptions instead. + $old_err = error_reporting(); + error_reporting($old_err & ~E_WARNING); + $doc->loadXML($xml); + error_reporting($old_err); $feed = $doc->documentElement; - if ($feed->namespaceURI != Activity::ATOM || + if (!$feed || + $feed->namespaceURI != Activity::ATOM || $feed->localName != 'feed') { throw new ClientException(_("Not an atom feed.")); } -- cgit v1.2.3 From 5fe83011299ac930def8e1db5f9eeae782ddb14c Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 22 Dec 2010 11:25:47 -0800 Subject: disable account deletion by default --- lib/default.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/default.php b/lib/default.php index 7a44ed875..6d57c4ef0 100644 --- a/lib/default.php +++ b/lib/default.php @@ -126,7 +126,7 @@ $default = 'biolimit' => null, 'backup' => true, 'restore' => true, - 'delete' => true, + 'delete' => false, 'move' => true), 'avatar' => array('server' => null, -- cgit v1.2.3 From 3e82000d578cf5f5935d972a26c84fe31768460a Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 22 Dec 2010 12:02:50 -0800 Subject: initialize ActivityObject::$extra --- lib/activityobject.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/activityobject.php b/lib/activityobject.php index 61614935f..fc7da7ee8 100644 --- a/lib/activityobject.php +++ b/lib/activityobject.php @@ -106,6 +106,10 @@ class ActivityObject public $largerImage; public $description; + // Extra stuff, that may need to be serialized + + public $extra = array(); + /** * Constructor * -- cgit v1.2.3 From 448dfb69d433c2e244b32ef1ba0af48b6d91ca23 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 22 Dec 2010 12:03:05 -0800 Subject: Initialize $extra member to empty array on ActivityObject --- lib/activityobject.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/activityobject.php b/lib/activityobject.php index 61614935f..5185d7761 100644 --- a/lib/activityobject.php +++ b/lib/activityobject.php @@ -105,6 +105,7 @@ class ActivityObject public $thumbnail; public $largerImage; public $description; + public $extra = array(); /** * Constructor -- cgit v1.2.3 From 35d9a065fb0f1ada1a96034d2e5a4076420d4e8a Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 22 Dec 2010 12:07:13 -0800 Subject: Revert "initialize ActivityObject::$extra" This reverts commit 3e82000d578cf5f5935d972a26c84fe31768460a. --- lib/activityobject.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/activityobject.php b/lib/activityobject.php index b86e6a984..5185d7761 100644 --- a/lib/activityobject.php +++ b/lib/activityobject.php @@ -107,10 +107,6 @@ class ActivityObject public $description; public $extra = array(); - // Extra stuff, that may need to be serialized - - public $extra = array(); - /** * Constructor * -- cgit v1.2.3 From 464e0f8115e3b5b01b6110ddc7a73274164c8584 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 22 Dec 2010 13:56:19 -0800 Subject: Don't trust text/xml mime types; generic content detection gives useless stuff like that on SVG images! Todo: replace the extension check in this case with better content-based checks. --- lib/mediafile.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/mediafile.php b/lib/mediafile.php index a41d7c76b..caa902de5 100644 --- a/lib/mediafile.php +++ b/lib/mediafile.php @@ -362,7 +362,9 @@ class MediaFile // we'll try detecting a type from its extension... $unclearTypes = array('application/octet-stream', 'application/vnd.ms-office', - 'application/zip'); + 'application/zip', + // TODO: for XML we could do better content-based sniffing too + 'text/xml'); if ($originalFilename && (!$filetype || in_array($filetype, $unclearTypes))) { $type = $mte->getMIMEType($originalFilename); -- cgit v1.2.3 From d5c2b0d21695f2ae661fa7ef4bbfbe9db0f66925 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 22 Dec 2010 14:55:13 -0800 Subject: When queueing is off, restore runs immediately. Indicate that we've already finished processing on the success page in this case; otherwise continue to show the 'will take a few minutes' message. --- actions/restoreaccount.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/actions/restoreaccount.php b/actions/restoreaccount.php index 5c8e4a12c..8cf220a42 100644 --- a/actions/restoreaccount.php +++ b/actions/restoreaccount.php @@ -48,6 +48,7 @@ if (!defined('STATUSNET')) { class RestoreaccountAction extends Action { private $success = false; + private $inprogress = false; /** * Returns the title of the page @@ -208,8 +209,13 @@ class RestoreaccountAction extends Action $qm = QueueManager::get(); $qm->enqueue(array(common_current_user(), $xml, false), 'feedimp'); - $this->success = true; - + if ($qm instanceof UnQueueManager) { + // No active queuing means we've actually just completed the job! + $this->success = true; + } else { + // We've fed data into background queues, and it's probably still running. + $this->inprogress = true; + } $this->showPage(); } catch (Exception $e) { @@ -228,6 +234,9 @@ class RestoreaccountAction extends Action function showContent() { if ($this->success) { + $this->element('p', null, + _('Feed has been restored. Your old posts should now appear in search and your profile page.')); + } else if ($this->inprogress) { $this->element('p', null, _('Feed will be restored. Please wait a few minutes for results.')); } else { -- cgit v1.2.3 From 8babcc2ad21f70745ce261476ffe74b42603419a Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 22 Dec 2010 15:04:50 -0800 Subject: Makefile to compress LinkPreview's js --- plugins/LinkPreview/Makefile | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 plugins/LinkPreview/Makefile diff --git a/plugins/LinkPreview/Makefile b/plugins/LinkPreview/Makefile new file mode 100644 index 000000000..6c8a03e94 --- /dev/null +++ b/plugins/LinkPreview/Makefile @@ -0,0 +1,11 @@ +.fake: all clean + +TARGETS=linkpreview.min.js + +all: $(TARGETS) + +clean: + rm -f $(TARGETS) + +linkpreview.min.js: linkpreview.js + yui-compressor $< -o $@ -- cgit v1.2.3 From e0606d3eca761bda5fb804f6522028280d3396e8 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 22 Dec 2010 15:20:07 -0800 Subject: Break xbImportNode.js and geometa.js back out of util.js; the Makefile in js has been updated to combine them with util.js source when building util.min.js Revert "combine our standard scripts into one big script" This reverts parts of commit 0c5ca46ba3a070803d993b0244fcc69d33875ebd. --- js/Makefile | 5 +- js/geometa.js | 217 ++++++++++++++++++++++++++++++++++++++++++ js/util.js | 269 ----------------------------------------------------- js/xbImportNode.js | 47 ++++++++++ 4 files changed, 267 insertions(+), 271 deletions(-) create mode 100644 js/geometa.js create mode 100644 js/xbImportNode.js diff --git a/js/Makefile b/js/Makefile index 00e434714..e7ae44e42 100644 --- a/js/Makefile +++ b/js/Makefile @@ -1,11 +1,12 @@ .fake: all clean TARGETS=util.min.js +SOURCES=util.js xbImportNode.js geometa.js all: $(TARGETS) clean: rm -f $(TARGETS) -util.min.js: util.js - yui-compressor $< -o $@ +util.min.js: $(SOURCES) + cat $+ | yui-compressor --type js > $@ diff --git a/js/geometa.js b/js/geometa.js new file mode 100644 index 000000000..bba59b448 --- /dev/null +++ b/js/geometa.js @@ -0,0 +1,217 @@ +// A shim to implement the W3C Geolocation API Specification using Gears or the Ajax API +if (typeof navigator.geolocation == "undefined" || navigator.geolocation.shim ) { (function(){ + +// -- BEGIN GEARS_INIT +(function() { + // We are already defined. Hooray! + if (window.google && google.gears) { + return; + } + + var factory = null; + + // Firefox + if (typeof GearsFactory != 'undefined') { + factory = new GearsFactory(); + } else { + // IE + try { + factory = new ActiveXObject('Gears.Factory'); + // privateSetGlobalObject is only required and supported on WinCE. + if (factory.getBuildInfo().indexOf('ie_mobile') != -1) { + factory.privateSetGlobalObject(this); + } + } catch (e) { + // Safari + if ((typeof navigator.mimeTypes != 'undefined') && navigator.mimeTypes["application/x-googlegears"]) { + factory = document.createElement("object"); + factory.style.display = "none"; + factory.width = 0; + factory.height = 0; + factory.type = "application/x-googlegears"; + document.documentElement.appendChild(factory); + } + } + } + + // *Do not* define any objects if Gears is not installed. This mimics the + // behavior of Gears defining the objects in the future. + if (!factory) { + return; + } + + // Now set up the objects, being careful not to overwrite anything. + // + // Note: In Internet Explorer for Windows Mobile, you can't add properties to + // the window object. However, global objects are automatically added as + // properties of the window object in all browsers. + if (!window.google) { + google = {}; + } + + if (!google.gears) { + google.gears = {factory: factory}; + } +})(); +// -- END GEARS_INIT + +var GearsGeoLocation = (function() { + // -- PRIVATE + var geo = google.gears.factory.create('beta.geolocation'); + + var wrapSuccess = function(callback, self) { // wrap it for lastPosition love + return function(position) { + callback(position); + self.lastPosition = position; + }; + }; + + // -- PUBLIC + return { + shim: true, + + type: "Gears", + + lastPosition: null, + + getCurrentPosition: function(successCallback, errorCallback, options) { + var self = this; + var sc = wrapSuccess(successCallback, self); + geo.getCurrentPosition(sc, errorCallback, options); + }, + + watchPosition: function(successCallback, errorCallback, options) { + geo.watchPosition(successCallback, errorCallback, options); + }, + + clearWatch: function(watchId) { + geo.clearWatch(watchId); + }, + + getPermission: function(siteName, imageUrl, extraMessage) { + geo.getPermission(siteName, imageUrl, extraMessage); + } + + }; +}); + +var AjaxGeoLocation = (function() { + // -- PRIVATE + var loading = false; + var loadGoogleLoader = function() { + if (!hasGoogleLoader() && !loading) { + loading = true; + var s = document.createElement('script'); + s.src = (document.location.protocol == "https:"?"https://":"http://") + 'www.google.com/jsapi?callback=_google_loader_apiLoaded'; + s.type = "text/javascript"; + document.getElementsByTagName('body')[0].appendChild(s); + } + }; + + var queue = []; + var addLocationQueue = function(callback) { + queue.push(callback); + }; + + var runLocationQueue = function() { + if (hasGoogleLoader()) { + while (queue.length > 0) { + var call = queue.pop(); + call(); + } + } + }; + + window['_google_loader_apiLoaded'] = function() { + runLocationQueue(); + }; + + var hasGoogleLoader = function() { + return (window['google'] && google['loader']); + }; + + var checkGoogleLoader = function(callback) { + if (hasGoogleLoader()) { return true; } + + addLocationQueue(callback); + + loadGoogleLoader(); + + return false; + }; + + loadGoogleLoader(); // start to load as soon as possible just in case + + // -- PUBLIC + return { + shim: true, + + type: "ClientLocation", + + lastPosition: null, + + getCurrentPosition: function(successCallback, errorCallback, options) { + var self = this; + if (!checkGoogleLoader(function() { + self.getCurrentPosition(successCallback, errorCallback, options); + })) { return; } + + if (google.loader.ClientLocation) { + var cl = google.loader.ClientLocation; + + var position = { + coords: { + latitude: cl.latitude, + longitude: cl.longitude, + altitude: null, + accuracy: 43000, // same as Gears accuracy over wifi? + altitudeAccuracy: null, + heading: null, + speed: null + }, + // extra info that is outside of the bounds of the core API + address: { + city: cl.address.city, + country: cl.address.country, + country_code: cl.address.country_code, + region: cl.address.region + }, + timestamp: new Date() + }; + + successCallback(position); + + this.lastPosition = position; + } else if (errorCallback === "function") { + errorCallback({ code: 3, message: "Using the Google ClientLocation API and it is not able to calculate a location."}); + } + }, + + watchPosition: function(successCallback, errorCallback, options) { + this.getCurrentPosition(successCallback, errorCallback, options); + + var self = this; + var watchId = setInterval(function() { + self.getCurrentPosition(successCallback, errorCallback, options); + }, 10000); + + return watchId; + }, + + clearWatch: function(watchId) { + clearInterval(watchId); + }, + + getPermission: function(siteName, imageUrl, extraMessage) { + // for now just say yes :) + return true; + } + + }; +}); + +// If you have Gears installed use that, else use Ajax ClientLocation +navigator.geolocation = (window.google && google.gears) ? GearsGeoLocation() : AjaxGeoLocation(); + +})(); +} diff --git a/js/util.js b/js/util.js index d929e91e2..eace1778e 100644 --- a/js/util.js +++ b/js/util.js @@ -1242,272 +1242,3 @@ $(document).ready(function(){ SN.Init.Login(); } }); - -// Formerly in xbImportNode.js -// @fixme put it back there -- since we're minifying we can concat in the makefile now - -/* is this stuff defined? */ -if (!document.ELEMENT_NODE) { - document.ELEMENT_NODE = 1; - document.ATTRIBUTE_NODE = 2; - document.TEXT_NODE = 3; - document.CDATA_SECTION_NODE = 4; - document.ENTITY_REFERENCE_NODE = 5; - document.ENTITY_NODE = 6; - document.PROCESSING_INSTRUCTION_NODE = 7; - document.COMMENT_NODE = 8; - document.DOCUMENT_NODE = 9; - document.DOCUMENT_TYPE_NODE = 10; - document.DOCUMENT_FRAGMENT_NODE = 11; - document.NOTATION_NODE = 12; -} - -document._importNode = function(node, allChildren) { - /* find the node type to import */ - switch (node.nodeType) { - case document.ELEMENT_NODE: - /* create a new element */ - var newNode = document.createElement(node.nodeName); - /* does the node have any attributes to add? */ - if (node.attributes && node.attributes.length > 0) - /* add all of the attributes */ - for (var i = 0, il = node.attributes.length; i < il;) { - if (node.attributes[i].nodeName == 'class') { - newNode.className = node.getAttribute(node.attributes[i++].nodeName); - } else { - newNode.setAttribute(node.attributes[i].nodeName, node.getAttribute(node.attributes[i++].nodeName)); - } - } - /* are we going after children too, and does the node have any? */ - if (allChildren && node.childNodes && node.childNodes.length > 0) - /* recursively get all of the child nodes */ - for (var i = 0, il = node.childNodes.length; i < il;) - newNode.appendChild(document._importNode(node.childNodes[i++], allChildren)); - return newNode; - break; - case document.TEXT_NODE: - case document.CDATA_SECTION_NODE: - case document.COMMENT_NODE: - return document.createTextNode(node.nodeValue); - break; - } -}; - -// @fixme put this next bit back too -- since we're minifying we can concat in the makefile now -// A shim to implement the W3C Geolocation API Specification using Gears or the Ajax API -if (typeof navigator.geolocation == "undefined" || navigator.geolocation.shim ) { (function(){ - -// -- BEGIN GEARS_INIT -(function() { - // We are already defined. Hooray! - if (window.google && google.gears) { - return; - } - - var factory = null; - - // Firefox - if (typeof GearsFactory != 'undefined') { - factory = new GearsFactory(); - } else { - // IE - try { - factory = new ActiveXObject('Gears.Factory'); - // privateSetGlobalObject is only required and supported on WinCE. - if (factory.getBuildInfo().indexOf('ie_mobile') != -1) { - factory.privateSetGlobalObject(this); - } - } catch (e) { - // Safari - if ((typeof navigator.mimeTypes != 'undefined') && navigator.mimeTypes["application/x-googlegears"]) { - factory = document.createElement("object"); - factory.style.display = "none"; - factory.width = 0; - factory.height = 0; - factory.type = "application/x-googlegears"; - document.documentElement.appendChild(factory); - } - } - } - - // *Do not* define any objects if Gears is not installed. This mimics the - // behavior of Gears defining the objects in the future. - if (!factory) { - return; - } - - // Now set up the objects, being careful not to overwrite anything. - // - // Note: In Internet Explorer for Windows Mobile, you can't add properties to - // the window object. However, global objects are automatically added as - // properties of the window object in all browsers. - if (!window.google) { - google = {}; - } - - if (!google.gears) { - google.gears = {factory: factory}; - } -})(); -// -- END GEARS_INIT - -var GearsGeoLocation = (function() { - // -- PRIVATE - var geo = google.gears.factory.create('beta.geolocation'); - - var wrapSuccess = function(callback, self) { // wrap it for lastPosition love - return function(position) { - callback(position); - self.lastPosition = position; - }; - }; - - // -- PUBLIC - return { - shim: true, - - type: "Gears", - - lastPosition: null, - - getCurrentPosition: function(successCallback, errorCallback, options) { - var self = this; - var sc = wrapSuccess(successCallback, self); - geo.getCurrentPosition(sc, errorCallback, options); - }, - - watchPosition: function(successCallback, errorCallback, options) { - geo.watchPosition(successCallback, errorCallback, options); - }, - - clearWatch: function(watchId) { - geo.clearWatch(watchId); - }, - - getPermission: function(siteName, imageUrl, extraMessage) { - geo.getPermission(siteName, imageUrl, extraMessage); - } - - }; -}); - -var AjaxGeoLocation = (function() { - // -- PRIVATE - var loading = false; - var loadGoogleLoader = function() { - if (!hasGoogleLoader() && !loading) { - loading = true; - var s = document.createElement('script'); - s.src = (document.location.protocol == "https:"?"https://":"http://") + 'www.google.com/jsapi?callback=_google_loader_apiLoaded'; - s.type = "text/javascript"; - document.getElementsByTagName('body')[0].appendChild(s); - } - }; - - var queue = []; - var addLocationQueue = function(callback) { - queue.push(callback); - }; - - var runLocationQueue = function() { - if (hasGoogleLoader()) { - while (queue.length > 0) { - var call = queue.pop(); - call(); - } - } - }; - - window['_google_loader_apiLoaded'] = function() { - runLocationQueue(); - }; - - var hasGoogleLoader = function() { - return (window['google'] && google['loader']); - }; - - var checkGoogleLoader = function(callback) { - if (hasGoogleLoader()) { return true; } - - addLocationQueue(callback); - - loadGoogleLoader(); - - return false; - }; - - loadGoogleLoader(); // start to load as soon as possible just in case - - // -- PUBLIC - return { - shim: true, - - type: "ClientLocation", - - lastPosition: null, - - getCurrentPosition: function(successCallback, errorCallback, options) { - var self = this; - if (!checkGoogleLoader(function() { - self.getCurrentPosition(successCallback, errorCallback, options); - })) { return; } - - if (google.loader.ClientLocation) { - var cl = google.loader.ClientLocation; - - var position = { - coords: { - latitude: cl.latitude, - longitude: cl.longitude, - altitude: null, - accuracy: 43000, // same as Gears accuracy over wifi? - altitudeAccuracy: null, - heading: null, - speed: null - }, - // extra info that is outside of the bounds of the core API - address: { - city: cl.address.city, - country: cl.address.country, - country_code: cl.address.country_code, - region: cl.address.region - }, - timestamp: new Date() - }; - - successCallback(position); - - this.lastPosition = position; - } else if (errorCallback === "function") { - errorCallback({ code: 3, message: "Using the Google ClientLocation API and it is not able to calculate a location."}); - } - }, - - watchPosition: function(successCallback, errorCallback, options) { - this.getCurrentPosition(successCallback, errorCallback, options); - - var self = this; - var watchId = setInterval(function() { - self.getCurrentPosition(successCallback, errorCallback, options); - }, 10000); - - return watchId; - }, - - clearWatch: function(watchId) { - clearInterval(watchId); - }, - - getPermission: function(siteName, imageUrl, extraMessage) { - // for now just say yes :) - return true; - } - - }; -}); - -// If you have Gears installed use that, else use Ajax ClientLocation -navigator.geolocation = (window.google && google.gears) ? GearsGeoLocation() : AjaxGeoLocation(); - -})(); -} diff --git a/js/xbImportNode.js b/js/xbImportNode.js new file mode 100644 index 000000000..f600a4789 --- /dev/null +++ b/js/xbImportNode.js @@ -0,0 +1,47 @@ +/* is this stuff defined? */ +if (!document.ELEMENT_NODE) { + document.ELEMENT_NODE = 1; + document.ATTRIBUTE_NODE = 2; + document.TEXT_NODE = 3; + document.CDATA_SECTION_NODE = 4; + document.ENTITY_REFERENCE_NODE = 5; + document.ENTITY_NODE = 6; + document.PROCESSING_INSTRUCTION_NODE = 7; + document.COMMENT_NODE = 8; + document.DOCUMENT_NODE = 9; + document.DOCUMENT_TYPE_NODE = 10; + document.DOCUMENT_FRAGMENT_NODE = 11; + document.NOTATION_NODE = 12; +} + +document._importNode = function(node, allChildren) { + /* find the node type to import */ + switch (node.nodeType) { + case document.ELEMENT_NODE: + /* create a new element */ + var newNode = document.createElement(node.nodeName); + /* does the node have any attributes to add? */ + if (node.attributes && node.attributes.length > 0) + /* add all of the attributes */ + for (var i = 0, il = node.attributes.length; i < il;) { + if (node.attributes[i].nodeName == 'class') { + newNode.className = node.getAttribute(node.attributes[i++].nodeName); + } else { + newNode.setAttribute(node.attributes[i].nodeName, node.getAttribute(node.attributes[i++].nodeName)); + } + } + /* are we going after children too, and does the node have any? */ + if (allChildren && node.childNodes && node.childNodes.length > 0) + /* recursively get all of the child nodes */ + for (var i = 0, il = node.childNodes.length; i < il;) + newNode.appendChild(document._importNode(node.childNodes[i++], allChildren)); + return newNode; + break; + case document.TEXT_NODE: + case document.CDATA_SECTION_NODE: + case document.COMMENT_NODE: + return document.createTextNode(node.nodeValue); + break; + } +}; + -- cgit v1.2.3