]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch '0.9.x' into 1.0.x
authorBrion Vibber <brion@pobox.com>
Wed, 22 Dec 2010 23:25:38 +0000 (15:25 -0800)
committerBrion Vibber <brion@pobox.com>
Wed, 22 Dec 2010 23:25:38 +0000 (15:25 -0800)
23 files changed:
README
actions/backupaccount.php [new file with mode: 0644]
actions/deleteaccount.php [new file with mode: 0644]
actions/profilesettings.php
actions/restoreaccount.php [new file with mode: 0644]
classes/Notice.php
classes/Profile.php
js/Makefile
js/geometa.js [new file with mode: 0644]
js/util.js
js/xbImportNode.js [new file with mode: 0644]
lib/activityimporter.php [new file with mode: 0644]
lib/activityobject.php
lib/activityutils.php
lib/default.php
lib/feedimporter.php [new file with mode: 0644]
lib/mediafile.php
lib/queuemanager.php
lib/right.php
lib/router.php
plugins/LinkPreview/Makefile [new file with mode: 0644]
plugins/OStatus/classes/Ostatus_profile.php
scripts/restoreuser.php

diff --git a/README b/README
index 3d23e2393bb81ee39dbf0482377825d873fe3145..35a055c77b6a98b89feda2e2389d32b638087833 100644 (file)
--- a/README
+++ b/README
@@ -1274,6 +1274,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 (file)
index 0000000..4f6fb93
--- /dev/null
@@ -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($argarray);
+
+        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 (file)
index 0000000..9abe2fc
--- /dev/null
@@ -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'));
+    }
+}
index 28b1d20f34125fab3b39b60281de0b08458dfeb2..8f55a471890931df2fd174bd9c85abd50537d8dc 100644 (file)
@@ -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 (file)
index 0000000..8cf220a
--- /dev/null
@@ -0,0 +1,376 @@
+<?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;
+    private $inprogress = 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($argarray);
+
+        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.
+            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.
+            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 = new DOMDocument();
+
+            // 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 ||
+                $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');
+
+            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) {
+            // 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 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 {
+            $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'));
+    }
+}
index 6acf0c7c1a97f57929cd0d37d65b8784eb183bf4..8d686ab651d8cc37e60ac2e938ad350eca10fdd5 100644 (file)
@@ -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;
     }
index 977947b6f7b8c8fed076441ecce39a50b1ade3a2..b7c1b06a8566c7869547f82286439b0db7ee286e 100644 (file)
@@ -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;
index 00e43471416e78b4e20d2e1aee2977fe815844b5..e7ae44e4214b6c556dc5655d46fa2cc8c140cfee 100644 (file)
@@ -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 (file)
index 0000000..bba59b4
--- /dev/null
@@ -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();
+
+})();
+}
index d929e91e2ee945874da126cca80316184580b0b1..eace1778e203362cc84980a6cc1b925abf06eaa0 100644 (file)
@@ -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 (file)
index 0000000..f600a47
--- /dev/null
@@ -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;
+       }
+};
+
diff --git a/lib/activityimporter.php b/lib/activityimporter.php
new file mode 100644 (file)
index 0000000..4a76781
--- /dev/null
@@ -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);
+    }
+}
index 61614935f1fa0ecb91f865a91f3a56e1b6b31638..5185d7761055951ab86a3cff18a8322c38384f25 100644 (file)
@@ -105,6 +105,7 @@ class ActivityObject
     public $thumbnail;
     public $largerImage;
     public $description;
+    public $extra = array();
 
     /**
      * Constructor
index c462514c498e40b04cea0f26f0eb5a5cd11e3b16..11befc0ed4774925c2b499eb540da72cc98324c0 100644 (file)
@@ -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;
+    }
 }
index 5c4484121d45620b15d1853c8035de5052f7d858..641528691b84e45a90c0c75a5fcb22786901af72 100644 (file)
@@ -124,7 +124,11 @@ $default =
               'featured' => array()),
         'profile' =>
         array('banned' => array(),
-              'biolimit' => null),
+              'biolimit' => null,
+              'backup' => true,
+              'restore' => true,
+              'delete' => false,
+              'move' => true),
         'avatar' =>
         array('server' => null,
               'dir' => INSTALLDIR . '/avatar/',
diff --git a/lib/feedimporter.php b/lib/feedimporter.php
new file mode 100644 (file)
index 0000000..e46858c
--- /dev/null
@@ -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;
+    }
+}
index a41d7c76b52d190e066e3b093570b67ad08d1f26..caa902de5dfeb13c18413372769198b9af209241 100644 (file)
@@ -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);
index 6666a6cb5a69974a61783a842ac3b106860998b4..60ac4855a703fbdb7339c0c27f175a196111d20d 100644 (file)
@@ -241,6 +241,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');
index bacbea5f2966dc6afd20d1708f92f57b58225e8a..5bf9c41161adf86972068d7ab8196975b9dac6a8 100644 (file)
@@ -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';
 }
 
index b7159e613027fc42a2a91a932e1b526aa49039ba..b9698294925a94a337be3ef95ff76d6b5e814639 100644 (file)
@@ -208,6 +208,9 @@ class Router
                           'deleteuser',
                           'geocode',
                           'version',
+                          'backupaccount',
+                          'deleteaccount',
+                          'restoreaccount',
             );
 
             foreach ($main as $a) {
diff --git a/plugins/LinkPreview/Makefile b/plugins/LinkPreview/Makefile
new file mode 100644 (file)
index 0000000..6c8a03e
--- /dev/null
@@ -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 $@
index 1089f6a21fd1b42ac797b262d0a2b9718a09fb49..c975cdf89c953a5447612de71a2724f46841da16 100644 (file)
@@ -897,54 +897,19 @@ class Ostatus_profile extends Managed_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);
     }
 
     /**
index b37e9db74108752f4b00072e212079b948b18b5d..17f007b41206406c36d13e6711f7bda7d8535d71 100644 (file)
@@ -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);