summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvan Prodromou <evan@status.net>2010-12-22 11:22:51 -0800
committerEvan Prodromou <evan@status.net>2010-12-22 11:22:51 -0800
commit9a6ceb3303a98d1c5fba3587f32d0377e55062cc (patch)
tree0b0348ff96d8c8f4c6f11ea60a9beee66065e479
parenta16131526b5ae77c558fe6b11d1726af39d7c82d (diff)
parent573bbeced10f06951db8875db8b4f9f0d0deca41 (diff)
Merge branch 'righttoleave' into 0.9.x
-rw-r--r--README6
-rw-r--r--actions/backupaccount.php260
-rw-r--r--actions/deleteaccount.php319
-rw-r--r--actions/profilesettings.php29
-rw-r--r--actions/restoreaccount.php359
-rw-r--r--classes/Notice.php11
-rw-r--r--classes/Profile.php12
-rw-r--r--lib/activityimporter.php350
-rw-r--r--lib/activityutils.php47
-rw-r--r--lib/default.php6
-rw-r--r--lib/feedimporter.php160
-rw-r--r--lib/queuemanager.php2
-rw-r--r--lib/right.php4
-rw-r--r--lib/router.php3
-rw-r--r--plugins/OStatus/classes/Ostatus_profile.php51
-rw-r--r--scripts/restoreuser.php309
16 files changed, 1578 insertions, 350 deletions
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/actions/backupaccount.php b/actions/backupaccount.php
new file mode 100644
index 000000000..9454741f0
--- /dev/null
+++ b/actions/backupaccount.php
@@ -0,0 +1,260 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Download a backup of your own account to the browser
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Account
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @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 <evan@status.net>
+ * @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 <evan@status.net>
+ * @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 '.
+ '<a href="http://activitystrea.ms/">Activity Streams</a> '.
+ '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/deleteaccount.php b/actions/deleteaccount.php
new file mode 100644
index 000000000..9abe2fcdb
--- /dev/null
+++ b/actions/deleteaccount.php
@@ -0,0 +1,319 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Delete your own account
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Account
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @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 <evan@status.net>
+ * @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 <evan@status.net>
+ * @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 = _('<p>This will <strong>permanently delete</strong> '.
+ 'your account data from this server. </p>');
+
+ if ($cur->hasRight(Right::BACKUPACCOUNT)) {
+ $msg .= sprintf(_('<p>You are strongly advised to '.
+ '<a href="%s">back up your data</a>'.
+ ' before deletion.</p>'),
+ 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 28b1d20f3..8f55a4718 100644
--- a/actions/profilesettings.php
+++ b/actions/profilesettings.php
@@ -452,4 +452,33 @@ class ProfilesettingsAction extends AccountSettingsAction
return $other->id != $user->id;
}
}
+
+ function showAside() {
+ $user = common_current_user();
+
+ $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');
+ }
+ 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 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Restore a backup of your own account from the browser
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Account
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @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 <evan@status.net>
+ * @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 <evan@status.net>
+ * @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 '.
+ '<a href="http://activitystrea.ms/">Activity Streams</a> 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/classes/Notice.php b/classes/Notice.php
index 629b7089d..50909f970 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/classes/Profile.php b/classes/Profile.php
index fe1a070bd..972351a75 100644
--- a/classes/Profile.php
+++ b/classes/Profile.php
@@ -858,6 +858,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/activityimporter.php b/lib/activityimporter.php
new file mode 100644
index 000000000..4a7678132
--- /dev/null
+++ b/lib/activityimporter.php
@@ -0,0 +1,350 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * class to import activities as part of a user's timeline
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Cache
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @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 <evan@status.net>
+ * @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, $author, $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, $se->getMessage());
+ return false;
+ } catch (Exception $e) {
+ common_log(LOG_ERR, $e->getMessage());
+ return false;
+ }
+ return true;
+ }
+
+ function subscribeProfile($user, $author, $activity)
+ {
+ $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);
+
+ 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 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));
+ }
+ }
+
+ // XXX: largely cadged from Ostatus_profile::processNote()
+
+ function postNote($user, $author, $activity)
+ {
+ $note = $activity->objects[0];
+
+ $sourceUri = $note->id;
+
+ $notice = Notice::staticGet('uri', $sourceUri);
+
+ if (!empty($notice)) {
+
+ 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));
+ }
+ } else {
+ throw new ClientException("Not overwriting author info for non-trusted user.");
+ }
+ }
+
+ // 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(),
+ 'distribute' => false);
+
+ // 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;
+ }
+
+ common_log(LOG_INFO, "Saving notice {$options['uri']}");
+
+ $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)
+ {
+ require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
+
+ $config = array('safe' => 1,
+ 'deny_attribute' => 'id,style,on*');
+
+ return htmLawed($content, $config);
+ }
+}
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/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/',
diff --git a/lib/feedimporter.php b/lib/feedimporter.php
new file mode 100644
index 000000000..e46858cc5
--- /dev/null
+++ b/lib/feedimporter.php
@@ -0,0 +1,160 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Importer for feeds of activities
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Account
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @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);
+}
+
+/**
+ * Importer for feeds of activities
+ *
+ * Takes an XML file representing a feed of activities and imports each
+ * activity to the user in question.
+ *
+ * @category Account
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class FeedImporter extends QueueHandler
+{
+ /**
+ * Transport identifier
+ *
+ * @return string identifier for this queue handler
+ */
+
+ public function transport()
+ {
+ return 'feedimp';
+ }
+
+ function handle($data)
+ {
+ list($user, $xml, $trusted) = $data;
+
+ try {
+ $doc = DOMDocument::loadXML($xml);
+
+ $feed = $doc->documentElement;
+
+ if ($feed->namespaceURI != Activity::ATOM ||
+ $feed->localName != 'feed') {
+ throw new ClientException(_("Not an atom feed."));
+ }
+
+
+ $author = ActivityUtils::getFeedAuthor($feed);
+
+ if (empty($author)) {
+ throw new ClientException(_("No author in the feed."));
+ }
+
+ if (empty($user)) {
+ if ($trusted) {
+ $user = $this->userFromAuthor($author);
+ } else {
+ throw new ClientException(_("Can't import without a user."));
+ }
+ }
+
+ $activities = $this->getActivities($feed);
+
+ $qm = QueueManager::get();
+
+ 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;
+ }
+ }
+
+ 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);
+
+ if (empty($user)) {
+ $attrs =
+ array('nickname' => Ostatus_profile::getActivityObjectNickname($author),
+ 'uri' => $author->id);
+
+ $user = User::register($attrs);
+ }
+
+ $profile = $user->getProfile();
+ Ostatus_profile::updateProfile($profile, $author);
+
+ // FIXME: Update avatar
+ return $user;
+ }
+}
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');
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';
}
diff --git a/lib/router.php b/lib/router.php
index 9cd28a394..c8e1c365a 100644
--- a/lib/router.php
+++ b/lib/router.php
@@ -208,6 +208,9 @@ class Router
'deleteuser',
'geocode',
'version',
+ 'backupaccount',
+ 'deleteaccount',
+ 'restoreaccount',
);
foreach ($main as $a) {
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);
}
/**
diff --git a/scripts/restoreuser.php b/scripts/restoreuser.php
index b37e9db74..17f007b41 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 <activity:subject> 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,9 @@ try {
} catch (NoUserArgumentException $noae) {
$user = null;
}
- $doc = getActivityStreamDocument();
- importActivityStream($user, $doc);
+ $xml = getActivityStreamDocument();
+ $qm = QueueManager::get();
+ $qm->enqueue(array($user, $xml, true), 'feedimp');
} catch (Exception $e) {
print $e->getMessage()."\n";
exit(1);